Create a method/function to use within a view - asp.net-core

How can we write a function to use in a *.cshtml page. We used to be able to use #helper or #function within the view. How do we do this? For instance, I would like to write a recursive function to show all configuration values. How could I do this?
<dl>
#foreach(var k in config.GetSubKeys())
{
<dt>#k.Key</dt>
<dd>#config.Get(k.Key)</dd>
#* TODO How can we make this a helper function/recursive? *#
#foreach(var sk in config.GetSubKey(k.Key).GetSubKeys())
{
<dt>#sk.Key</dt>
<dd>#config.Get(sk.Key)</dd>
}
}
</dl>
I imagine that we need to add a dependency in project.json and then opt-in to using it in Startup.cs.

Quick and dirty using razor views assuming your view component provides a recursive model.
Component view:
#model YourRecursiveDataStructure
<ul class="sidebar-menu">
<li class="header">MAIN NAVIGATION</li>
#foreach (var node in Model.RootNodes)
{
#Html.Partial("~/YourPath/RenderElement.cshtml", node)
}
</ul>
Render element in a view :
#model YourRecursiveNode
<li>
<a href="#Model.Href">
<span>#Model.Display</span>
</a>
#Html.Partial("~/YourPath/RenderChildren.cshtml", Model)
</li>
Then loop node's children in another view:
#model YourRecursiveNode
#if (Model.HasChildren)
{
<ul>
#foreach (var child in Model.Children)
{
#Html.Partial("~/YourPath/RenderElement.cshtml", child)
}
</ul>
}

Referring to a few design discussions that we only have glimpses of online, #helper was removed for design reasons; the replacement is View Components.
I'd recommend a View Component that looked like the following:
public class ConfigurationKeysViewComponent : ViewComponent
{
private readonly IConfiguration config;
public ConfigurationKeysViewComponent(IConfiguration config)
{
this.config = config;
}
public IViewComponentResult Invoke(string currentSubKey = "")
{
return View(new ConfigurationData
{
Key = currentSubKey,
Value = config.Get(currentSubKey),
SubKeys = config.GetSubKey(currentSubKey).GetSubKeys().Select(sk => sk.Key)
});
}
}
Your ViewComponent's View would then be relatively simple:
<dt>#Model.Key</dt>
<dd>#config.Get(Model.Key)</dd>
#foreach (var sk in Model.SubKeys)
{
#Component.Invoke("ConfigurationKeys", sk)
}
You could then invoke it from your root view as follows:
#Component.Invoke("ConfigurationKeys")
Disclaimer: I wrote this in the SO editor, there may be compiler errors. Also, I'm uncertain if View Components support default parameters - you may need to add a default "" to the root view's call to the view component.
Alternatively, if this is just debugging code, you can unwrap your recursiveness by using a Stack<>.

Related

Position Blazor component inside user content

I have a requirement to dynamically put a Blazor component inside user-provided content. Essentially, the component is supposed to extend user-provided markup with some UI elements.
Let's say the user provides some content that can have a "greeting-container" element in it and the component should insert a greeting button inside that element.
My current solution is to call a JavaScript function to move the DOM element in OnAfterRenderAsync (full code below). It seems to work fine, but manipulating DOM elements seems to be discouraged in Blazor since it can affect the diffing algorithm. So I have a couple of questions on this:
How bad is it to move DOM elements like this? Does it cause performance issues, functional issues or some undefined behavior?
Is there a better way to achieve the same result without using JavaScript? I was considering using the RenderTreeBuilder for this, but it seems like it might not be designed for this purpose since it's recommended to use hardcoded sequence numbers, which doesn't seem possible when dealing with dynamic content not known at compilation time.
Current solution code:
Greeter.razor
#page "/greeter"
#inject IJSRuntime JSRuntime;
<div>
#((MarkupString)UserContentMarkup)
<div id="greeting">
<button #onclick="ToggleGreeting">Toggle greeting</button>
#if (isGreetingVisible) {
<p>Hello, #Name!</p>
}
</div>
</div>
#code {
[Parameter]
public string UserContentMarkup { get; set; }
[Parameter]
public string Name { get; set; }
private bool isGreetingVisible;
private void ToggleGreeting()
{
isGreetingVisible = !isGreetingVisible;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await JSRuntime.InvokeVoidAsync("moveGreetingToContainer");
}
}
_Host.cshtml
window.moveGreetingToContainer = () => {
var greeting = document.getElementById("greeting");
var container = document.getElementById("greeting-container");
container.appendChild(greeting);
}
UserContentTest.razor
#page "/userContentTest"
#inject IJSRuntime JSRuntime;
<h3>Testing user content</h3>
<Greeter UserContentMarkup=#userContentMarkup Name="John"></Greeter>
#code {
private string userContentMarkup = "Some <b>HTML</b> text followed by greeting <div id='greeting-container'></div> and <i>more</i> text";
}
Expected result (after clicking "Toggle greeting"):
<div>
Some <b>HTML</b> text followed by greeting
<div id="greeting-container">
<div id="greeting">
<button>Toggle greeting</button>
<p>Hello, John!</p>
</div>
</div> and <i>more</i> text
</div>
Great question - and yes, using JS to move the dom elements is very bad as Blazor doesn't see the change you made to the dom.
What you can do is switch over to using a RenderFragment and more specifically RenderFragment<RenderFragment> which is markup that will be supplied with more markup as a parameter.
On the second line, I am invoking the UserContentMarkup method (which is a RenderFragment<RenderFragment>) and passing in the <div id=greeting> content as the context parameter.
Note: It is wrapped in a <text> element which is actually a way to embed HTML in C# in a Razor file. It does not render a <text> element to the page.
Greeter.razor
<div>
#UserContentMarkup(
#<text>
<div id="greeting">
<button #onclick="ToggleGreeting">Toggle greeting</button>
#if (isGreetingVisible) {
<p>Hello, #Name!</p>
}
</div>
</text>
)
</div>
#code {
[Parameter]
public RenderFragment<RenderFragment> UserContentMarkup { get; set; }
[Parameter]
public string Name { get; set; }
private bool isGreetingVisible;
private void ToggleGreeting()
{
isGreetingVisible = !isGreetingVisible;
}
}
UserContentTest.razor
Here you can see two ways to consume Greeter - using markup in the page, or using a code method.
<h3>Testing user content</h3>
#* Using markup to supply user content - #context is where the button goes *#
<Greeter Name="John">
<UserContentMarkup>
Some <b>HTML</b> text followed by greeting
<div id='greeting-container'>#context</div> and <i>more</i> text
</UserContentMarkup>
</Greeter>
#* Using a method to supply the user content - #context is where the button goes *#
<Greeter Name="John" UserContentMarkup=#userContent />
This code method can be confusing - it is a RenderFragment<RenderFragment> which means it has to be a method that accepts a RenderFragment as its only parameter, and returns a RenderFragment - the RenderFragment being returned in this case is markup wrapped in <text> to make it clear it is markup.
#code
{
RenderFragment<RenderFragment> userContent
=> context => #<text>Some stuff #context more stuff</text>;
}
Try it out here : https://blazorrepl.com/repl/QuPPaMEu34yA5KSl40

In a view component invoked as a tag helper, how can we access the inner HTML?

In tag helpers, we can access the tags inner HTML.
<!-- Index.cshtml -->
<my-first>
This is the original Inner HTML from the *.cshtml file.
</my-first>
// MyFirstTagHelper.cs > ProcessAsync
var childContent = await output.GetChildContentAsync();
var innerHtml = childContent.GetContent();
If we invoke a view component as a tag helper, how can we access the inner HTML?
<vc:my-first>
This is the original Inner HTML from the *.cshtml file.
</vc:my-first>
// MyFirstViewComponent.cs > InvokeAsync()
var innerHtml = DoMagic();
A few further thoughts:
I appreciate that we can pass arbitrary content to a view component via HTML attributes. That can become impractical, though, in the case of very large amounts of text, in which case inner HTML would be more readable and have better tooling support.
Some might also say, "Why not just use a tag helper, then?" Well, we want to associate a view with the tag helper, and tag helpers do not support views; instead, tag helpers require us to build all the HTML programmatically.
So we're stuck in a bit of bind. On the one hand, we want a tag helper, but they don't support views; on the other hand, we can use a view component as a tag helper, but view components might not let us access the inner HTML.
Sorry, but the answer is "Just use a tag helper"
See the following issue: https://github.com/aspnet/Mvc/issues/5465
Being able to invoke a ViewComponent from a tag helper is just a different way to call it instead of using #Component.Invoke().
I can see where this is misleading, and I do recall seeing in the ASP.NET Core 2.1 tutorials a statement to the effect of "View Components are like Tag Helpers, but more powerful."
Finally, a way to have our cake and eat it too! Allow a tag-helper to use a Razor view as its source of HTML, yet still wrap markup when used in a Razor page.
Use a tag-helper to get the inner HTML as a string. Then directly operate the Razor view engine to render a partial view to a string. Finally, use string replacement to place the inner HTML string into the right place in the partial view string.
The key is to use the high-quality StackOverflow answers available on rendering a Razor view as a string. See the IRazorViewToStringRenderer service here (it says ASP.NET Core 3.1 but worked for me in 2.2), or elsewhere as Mvc.RenderViewToString.
The tag-helper:
// RazorViewTagHelper.cs
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace GetSafetyCone.TagHelpers
{
public class RazorViewTagHelper : TagHelper
{
private IRazorViewToStringRenderer RazorViewToStringRenderer { get; }
public ViewName { get; set; }
public RazorViewedTagHelperBase(
IRazorViewToStringRenderer razorViewToStringRenderer)
{
this.RazorViewToStringRenderer = razorViewToStringRenderer;
}
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
var childContent = await output.GetChildContentAsync();
var childContentString = childContent.GetContent();
var viewHtmlTemplateString = await this.RazorViewToStringRenderer.Render<object>(this.ViewName, null);
var viewHtmlString = viewHtmlTemplateString.Replace("BODY_GOES_HERE", childContentString);
output.Content.SetHtmlContent(viewHtmlString); // Set the content.
}
}
}
The partial view you want to use as the source of HTML for the tag-helper:
// ViewXYZ.cshtml
#*
ViewXYZ to be rendered to string.
*#
// No model specified, so ok model was a null object.
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="max-w-3xl mx-auto">
BODY_GOES_HERE
</div>
</div>
And here's the tag-helper in use:
// YourPage.cshtml
<razor-view view-name="ViewXYZ">
<p>You should see this content wrapped in the ViewXYZ divs.</p>
</razor-view>
If you want, you can simplify the string replacement and use the childContent TagHelperContent directly as the model of the view:
// RazorViewTagHelper.cs
...
var childContent = await output.GetChildContentAsync();
var viewHtmlString = await this.RazorViewToStringRenderer.Render("viewName", childContent);
...
// ViewXYZ.cshtml
...
#model Microsoft.AspNetCore.Razor.TagHelpers.TagHelperContent;
...
<div class="max-w-3xl mx-auto">
#(this.Model)
</div>
...

asp.net Core View Component executed but markup not rendered

I have an ASP.NET Core 1.1 web application developed with VS.2017 and I decided to put some of the view functionality in a view component (have done others before).
This view component fetches a Dictionary collection of permissions associated to a user ID and displays them in a nice table. When I put it as part of the page (not VC) it works. But when I use it as a view component the component's HTML is never rendered.
I placed a breakpoint in the view component and it triggers, I see the View(granted) return statement return a populated list so up until there execution is as expected.
Then I placed a breakpoint in the ViewComponent's default.cshtml code section at the top the #{ some code here } and that breakpoint triggers as well, so the view component's Default.cshtml file is found. This Default.cshtml has some markup to render a table, therein within the table I have a #foreach() statement and when do a "Run to cursor" to that precise location -the loop that iterates- it triggers as well so it is iterating through the collection.
But after all that the main view looks as if the view component isn't there, none of the HTML found in the Default.cshtml is rendered even though it was found and executed. What am I missing here? so far my impression has been that VS.2017 (with all its updates) is not very stable.
Default.cshtml
#using ACME.AspNetCore.Permissions.Infrastructure.Authorization
#model Dictionary<Permission, string>
#{
ViewBag.UserName = "xxx";
Model.Add(Permission.Permission1, "test");
}
<h1>Component Output</h1>
<div class="well well-lg">
<table class="table table-hover">
<thead>
<tr>
<th>Permission</th>
<th>Description</th>
<th class="text-center">Status</th>
<th class="text-center">Revoke it!</th>
</tr>
</thead>
<tbody>
#foreach (var dictentry in Model)
{
<tr>
<td>#dictentry.Key.ToString()</td>
<td>#dictentry.Value</td>
<td class="text-center"><span class="glyphicon glyphicon-ok" style="color:green;"></span></td>
<td class="text-center"><a asp-action="RevokePermission" asp-route-id="#ViewBag.UserName" asp-route-pid="#dictentry.Key.ToString()"><span class="glyphicon glyphicon-thumbs-down" style="color:red;"></span></a></td>
</tr>
}
</tbody>
<tfoot><p class="alert alert-success"><span class="glyphicon glyphicon-eye-open"></span> Granted permissions</p></tfoot>
</table>
</div>
GrantedPermissionsViewComponent.cs
[ViewComponent(Name = "GrantedPermissions")]
public class GrantedPermissionsViewComponent : ViewComponent {
private readonly ApplicationDbContext _context;
public GrantedPermissionsViewComponent(ApplicationDbContext context) : base()
{
_context = context;
}
public async Task<IViewComponentResult> InvokeAsync(string emailOrUserId)
{
string id;
Guid UID;
if (Guid.TryParse(emailOrUserId, out UID))
{ // THE PARAMETER WAS A GUID, THUS A USER ID FROM IDENTITY DATABASE
id = emailOrUserId;
}
else
{ // THE PARAMETER IS ASSUMED TO BE AN EMAIL/USERNAME FROM WHICH WE CAN DERIVE THE USER ID
id = _context.Users.Where(u => u.Email == emailOrUserId.Trim()).Select(s => s.Id).FirstOrDefault();
}
Dictionary<Permission, string> granted = GetOwnerPermissions(id);
return View(granted);
}
private Dictionary<Permission, string> GetOwnerPermissions(string userId)
{
Dictionary<Permission, string> granted;
granted = _context.UserPermissions.Where(u => u.ApplicationUserId == userId)
.Select(t => new { t.Permission })
.AsEnumerable() // to clients memory
.Select(o => new KeyValuePair<Permission, string>(o.Permission, o.Permission.Description()))
.ToList()
.ToDictionary(x => x.Key, x => x.Value);
return granted;
}
}
So why on earth is it triggering on the component's code as well as on the component's view (default.cshtml) and yet it does not render the HTML code found therein?
Component invokation in the main view:
#{await Component.InvokeAsync<GrantedPermissionsViewComponent>(
new { emailOrUserId = ViewBag.UserName });
}
NOTE
The InvokeAsync is actually executing synchronously (per warning) because I could not find a way to have GetOwnerPermissions to await on anything... But that is not the problem.
The problem lies in how you are invoking the ViewComponent.
If you use #{ ... } it means you want to execute code and not render to output.
If you use parenthesis instead of brackets, the result gets rendered to output. #( ... )
In your case, you don't even need the parenthesis.
Try invoking it has following:
#await Component.InvokeAsync("GrantedPermissions", new { emailOrUserId = ViewBag.UserName })
More info here
try this, your mileage might vary. :)
#if (User.Identity.IsAuthenticated)
{
<div>User: #User.Identity.Name</div>
#(await Component.InvokeAsync("DefaultNavbar"));
#RenderBody()
}
else
{
<div> show public pages</div>
#RenderBody()
}

Aurelia iterate over map where keys are strings

I'm having trouble getting Aurelia to iterate over a map where the keys are strings (UUIDs).
Here is an example of the data I'm getting from an API running somewhere else:
my_data = {
"5EI22GER7NE2XLDCPXPT5I2ABE": {
"my_property": "a value"
},
"XWBFODLN6FHGXN3TWF22RBDA7A": {
"my_property": "another value"
}
}
And I'm trying to use something like this:
<template>
<div class="my_class">
<ul class="list-group">
<li repeat.for="[key, value] of my_data" class="list-group-item">
<span>${key} - ${value.my_property}</span>
</li>
</ul>
</div>
</template>
But Aurelia is telling me that Value for 'my_data' is non-repeatable.
I've found various answer by googling, but they have not been clearly explained or incomplete. Either I'm googling wrong or a good SO question and answer is needed.
As another resource to the one supplied by ry8806, I also use a Value Converter:
export class KeysValueConverter {
toView(obj) {
if (obj !== null && typeof obj === 'object') {
return Reflect.ownKeys(obj).filter(x => x !== '__observers__');
} else {
return null;
}
}
}
It can easily be used to do what you're attempting, like this:
<template>
<div class="my_class">
<ul class="list-group">
<li repeat.for="key of my_data | keys" class="list-group-item">
<span>${key} - ${my_data[key]}</span>
</li>
</ul>
</div>
</template>
The easiest method would be to convert this into an array yourself (in the ViewModel code)
Or you could use a ValueConverter inside repeat.for as described in this article Iterating Objects
The code...
// A ValueConverter for iterating an Object's properties inside of a repeat.for in Aurelia
export class ObjectKeysValueConverter {
toView(obj) {
// Create a temporary array to populate with object keys
let temp = [];
// A basic for..in loop to get object properties
// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Statements/for...in
for (let prop in obj) {
if (obj.hasOwnProperty(prop)) {
temp.push(obj[prop]);
}
}
return temp;
}
}
/**
* Usage
* Shows how to use the custom ValueConverter to iterate an objects properties
* aka its keys.
*
* <require from="ObjectKeys"></require>
* <li repeat.for="prop of myVmObject | objectKeys">${prop}</li>
*/
OR, you could use the Aurelia Repeat Strategies provided by an Aurelia Core Team member
You'd have to import the plugin into your app.
Then you'd use it using the pipe syntax in your repeat.for....like so....
<div repeat.for="[key, value] of data | iterable">
${key} ${value.my_property}
</div>

kendo editor not responding after multiple requests to the same page in IE

I have a very weird bug. I have a page on MVC that displays two editors and gets passed a model with the value for both editors. The model is as follows:
public class BulletinsModel
{
[AllowHtml]
[Display(Name = "Some Bulletin")]
public string SomeBulletin { get; set; }
[AllowHtml]
[Display(Name = "Other Bulletin")]
public string OtherBulletin { get; set; }
}
I then, defined a view which receives this view model and maps it to two kendo editors.There is also some javascript code to make a post to update the information.
#model BulletinsModel
<div id="settings">
<div class="form-horizontal">
<div class="form-group">
#Html.LabelFor(m => m.SomeBulletin, new { #class = "col-md-6 text-left" })
#(Html.Kendo().EditorFor(m => m.SomeBulletin).Encode(false).Name("Some_Bulletin"))
#Html.LabelFor(m => m.OtherBulletin, new { #class = "col-md-6 text-left" })
#(Html.Kendo().EditorFor(m => m.OtherBulletin).Encode(false).Name("Other_Bulletin"))
</div>
</div>
</div>
My code for my action method that renders this view is as follows (nothing fancy):
[HttpGet]
public PartialViewResult Index()
{
ViewBag.ActiveSectionName = "Bulletins";
var bulletinModel = GetBulletinsModel();
return PartialView("_Bulletins",bulletinModel);
}
However, my issue is that after hitting the Index action a couple of times, the editors become non responsive and I cannot edit the information on them. This only happens on IE, as I have not been able to replicate the issue in other browsers.
EDIT: I have just noticed that the editor is frozen. In order to be able to edit what's inside of the editor I need to click on any option of the toolbar to make it responsive once again. Why is that?
Turns out that the issue is happening with IE as detailed in this post:
Adding, removing, adding editor -> all editors on page become read only in IE. The problem is with the iframes inside the editor. I was loading my page with an Ajax request to which I had to add the following code before making the request to make it work.
function unloadEditor($editor) {
if ($editor.length > 0) {
$editor.data('kendoEditor').wrapper.find("iframe").remove();
$editor.data('kendoEditor').destroy();
}
}
unloadEditor($('#myEditor'));