How to define templated component in RenderFragment with its #context available - razor

I have a templated component (a tooltip) that has a parameter and passes that parameter in context to child content. That parameter is a wrapper for ElementReference. The purpose of this is to get back to the tooltip the child's reference, once it is set.
What I want to do is to store a particular instance of that tooltip component in a reusable RenderFragment in several places.
But I get the error The name 'context' does not exists in the current context.
This is original question, but it proved to be oversimplified. Please go to second separation line, with sample more resembling my situation.
Here is a sample (not that tooltip, but simplified code that has exactly the same problem).
Templated component RenderFragTempl:
#inherits ComponentBase
<span id="#(Ref)">
#IntChildContent(Ref)
</span>
#code {
[Parameter] public RenderFragment<int> IntChildContent { get; set; }
[Parameter] public int Ref { get; set; }
}
and the call in Index.razor:
#page "/"
#using BlazorServer.Pages.StackOverflow
<RenderFragTempl Ref="10">
<IntChildContent>
<p>#context</p>
</IntChildContent>
</RenderFragTempl>
<br />
#test
#code {
//compiler error CS0103: The name 'context' does not exist in the current context
private readonly RenderFragment test = #<RenderFragTempl Ref="10001">
<IntChildContent>
<p>#context</p>
</IntChildContent>
</RenderFragTempl>;
}
EDIT: 1
I have a tooltip component that has a child content. Tooltip will be shown whenever the mouse hovers over that child content. But tooltip does not wrap the child content with anything. So if you check out the source, you will only see the child content without any indication of the tooltip. The moment mouse cursor hovers over the child content, the tooltip container is added to the bottom of the page and is position right over that child content. This is quite problematic to achieve in blazor because the tooltip needs to have the reference to the child content. But references are established after parameters are filled. So a special wrapper is used to achieve that and Tooltip is built as a templated component.
So the wrapper for ElementReference I use (curtesy of MatBlazor):
public class TargetForwardRef
{
private ElementReference _current;
public ElementReference Current
{
get => _current;
set
{
Set(value);
//this is just for debugging purpose
Console.WriteLine($"Ref: {value.Id ?? "null"}");
}
}
public void Set(ElementReference value) => _current = value;
}
My simplified Tooltip as RenderFragTempl (just the important bits)
#inherits ComponentBase
<span>
#UnboundChildContent(RefBack)
</span>
#code {
[Parameter] public RenderFragment<TargetForwardRef> UnboundChildContent { get; set; }
[Parameter] public TargetForwardRef RefBack { get; set; } = new TargetForwardRef();
}
And my index.razor
#page "/"
#*
//this is working, will printout to console `<span>` reference id, you will be
//able to find it once you go to source; I added this here for reference
*#
<RenderFragTempl>
<UnboundChildContent>
<span #ref="#context.Current">Span content</span>
</UnboundChildContent>
</RenderFragTempl>
<br/>
#test
#code {
//compiler error CS0103: The name 'context' does not exist in the current context
private readonly RenderFragment test =
#<RenderFragTempl>
<UnboundChildContent>
<span #ref="#context.Current">Hover to show tooltip</span >
</UnboundChildContent>
</RenderFragTempl>;
}
To answer the question - why I am trying to use RenderFragment - imagine I have a collection of cards - let's say 150. The card component accepts as a parameter IList<RenderFragment> for rendering buttons on a card. I want to pass my icon with tooltip to that card. I need to have access to Tooltip's context.
I tried renaming context to something else, but I get the same error (except in the error there is new context name).

Why define a templated component and use it like that ?
However, here's code sample that demonstrates how to get the same required result without defining a templated component:
#test(12)
#code {
private RenderFragment<int> test => (value) => (__builder) =>
{
<span id="#(value)">
#value
</span>
};
}
But if you insist, here's the code that demonstrate how to render your component:
#test((10001, 15))
#code
{
private RenderFragment<(int, int)> test => (value) => (__builder) =>
{
<RenderFragTempl Ref="#value.Item1">
<IntChildContent>
<p>#value.Item2</p>
</IntChildContent>
</RenderFragTempl>;
};
}
Update:
The following code describes how to render the templated component with the internal variable context. You can improve on it as you like.
Note: I did not read your update... I just do what you requested on the original question, but this time using context.
#test(121)
#code {
private RenderFragment<int> test => (value) => (__builder) =>
{
__builder.OpenComponent<TemplatedComponent>(0);
__builder.AddAttribute(1, "Ref", value);
__builder.AddAttribute(2, "IntChildContent",
(RenderFragment<int>)((context) => (__builder2) =>
{
__builder2.OpenElement(3, "p");
__builder2.AddContent(4, context);
__builder2.CloseElement();
}
));
__builder.CloseComponent();
};
}
Note that when you invoke the RenderFragment delegate you pass it a value which is assigned to the Ref parameter, and is passed in the form the the internal context variable

I asked this question in https://github.com/dotnet/aspnetcore/issues/29671 and I got an answer. This is not exactly what I wanted, but that seems to be official:
private readonly RenderFragment<TargetForwardRef> test => context =>
#<RenderFragTempl>
<UnboundChildContent>
<span #ref="#context.Current">Hover to show tooltip</span >
</UnboundChildContent>
</RenderFragTempl>;

Related

How to Assign Async Value to Razor Component [Parameter] Property?

I am attempting to create my first Razor Component in a Blazor Server-side project. The Razor Component is named MyComponent and has a property configured to retrieve its value from input:
MyComponent.razor
[Parameter]
public int Count {get; set;}
I am pulling the count from an injected service configured via IServiceCollection, which looks like this:
public interface ICountingService
{
ValueTask<int> Get();
}
The hosting page, Index.razor looks like the following:
#page "/"
#inject ICountingService Counter
<h1>Hello World!</h1>
<MyComponent Count="#Counter.Get()" />
However, I cannot seem to bind the correct value for the Count property.
I get the following error:
cannot convert from 'System.Threading.Tasks.ValueTask<int>' to 'int'
All of the examples I have found for assigning [Parameter] values to Razor Components are synchronous, and the only asynchronous values I have found are for callbacks and methods (not parameters).
Further, searching online did not return anything obvious so I am posting here in hopes of finding an answer.
Note that I am aware of using protected override async Task OnInitializedAsync and storing a value in there, but that seems like a lot of required ceremony compared to the approach above, especially when considering the multiple services and properties that I will ultimately have to bind.
So, how does one assign values from an asynchronous call to a Razor Component [Parameter] property in the way that I would prefer?
The problem is, Counter.Get() isn't an int value; it's a Task that will have an int at some undefined point either now or in the future. So you can't assign its value to something that's expecting an int right now, because that int doesn't necessarily exist yet.
You've already got the answer, and though it "seems like a lot of ceremony", it's really the only way to do this:
Create an int property to hold the value.
Declare an async method
In that method, assign the awaited value of Counter.Get() to the int that's holding the value
Set the component's Count property equal to the int property
It may feel like a lot of ceremony, but you should be grateful. Asynchrony is inherently very complicated, and having async/await available already takes care of about 95% of the hard work for you. If you think this solution is messy, you oughtta see what it would take to get it right without async/await!
Try this.
#page "/"
#inject ICountingService Counter
<h1>Hello World!</h1>
<MyComponent Count="#CounterValue" />
#code{
public int CounterValue {get; set;}
protected override async Task OnInitializedAsync()
{
CounterValue = await Counter.Get();
}
}
After #mason-wheeler and #rich-bryant provided their answers, I went to think about this a little more and found my solution, which I have posted here:
https://github.com/Mike-E-angelo/Blazor.ViewProperties
I am calling it a ViewProperty which looks like the following:
public interface IViewProperty
{
ValueTask Get();
}
public sealed class ViewProperty<T> : IViewProperty
{
public static implicit operator ViewProperty<T>(ValueTask<T> instance) => new ViewProperty<T>(instance);
readonly ValueTask<T> _source;
public ViewProperty(ValueTask<T> source) => _source = source;
public T Value { get; private set; }
public bool HasValue { get; private set; }
public async ValueTask Get()
{
Value = await _source;
HasValue = true;
}
public override string ToString() => Value.ToString();
}
You then pair it with a component base type that then iterates through the component's view properties and invokes their respective asynchronous operations:
public abstract class ViewPropertyComponentBase : ComponentBase
{
protected override async Task OnParametersSetAsync()
{
var properties = GetType().GetRuntimeProperties();
foreach (var metadata in properties.Where(x => x.GetCustomAttributes<ParameterAttribute>().Any() &&
typeof(IViewProperty).IsAssignableFrom(x.PropertyType)))
{
if (metadata.GetValue(this) is IViewProperty property)
{
await property.Get().ConfigureAwait(false);
}
}
}
}
A sample razor component that uses the above:
MyComponent.razor
#inherits ViewPropertyComponentBase
#if (Count.HasValue)
{
<p>Your magic number is #Count.</p>
}
else
{
<p>Loading, please wait...</p>
}
#code {
[Parameter]
public ViewProperty<int> Count { get; set; }
}
The resulting use is a clean view with direct bindings and no need for overrides or other additional ceremony:
#page "/"
#inject ICounter Count
<h1>Hello, world!</h1>
Welcome to your new app.
<MyComponent Count="#Count.Count()" />
(NOTE that my posted example and above uses reflection, which is slow. In the actual version of the solution that I am using, I compile the member access as lambda expressions and cache the result. You can find that by starting here if you are brave enough to poke around.)
It feels a bit hacky, but you could do something like this:
<MyComponent Count="#Counter.Get().Result" />

Use AdditionalFields to compare to field in a different class

Introduction
In MVC Core I have a base ViewModel and two ViewModels included in the base model as properties, like so:
public class BaseViewModel
{
public FirstViewModel First { get; set; }
public SecondViewModel Second { get; set; }
}
In FirstViewModel I added a custom validation attribute on one of the properties, inheriting from RemoteAttribute. My goal is to use this attribute comparing the value to a property in SecondViewModel. I've set this up using the AdditionalFields property of the RemoteAttribute.
I think my problem lies in the way the HTML attributes are added to the control in the razor view:
data-val-remote-additionalfields="*.PropOfModelFirst,*.PropOfModelSecond"
When the clientside validation is calling the controller action, the *. is replaced by the framework by First., which is wrong, because the second value is not part of the First-class.
I tried prepending the classname to the second property, resulting in
data-val-remote-additionalfields="*.PropOfModelFirst,*.Second.PropOfModelSecond"
but as can be expected this is changed to First.Second.PropOfModelSecond.
Question
Can the AdditionalFields property be used to compare against values from another ViewModel?
You cannot use AdditionalFields to compare against values from another ViewModel. The reason is that the rules are added to jquery.validate.js by the jquery.validate.unobtrusive.js plugin (which reads the data-val-* attributes generated by the HtmlHelper methods). Specifically it is the adapters.add("remote", ["url", "type", "additionalfields"], function (options) { method that is pre-pending First to the property names.
One option would be to use a single 'flat' view model containing all properties.
If that is not desirable, then you can just write your own ajax code to call your server method that performs the validation. This actually has some added performance benefits as well. By default, after initial validation triggered by the .blur() event, validation is performed on every .keyup() event, meaning that you are potentially making a lot of ajax and database calls if the user initially entered an invalid value.
Remove the [Remote] attribute, and add the following script (I'll assume the properties are First.ABC and Second.XYZ)
$('#First_ABC').change(function() {
var url = '#Url.Action(...)'; // add your action name
var input = $(this);
var message = $('[data-valmsg-for="First.ABC"]'); // or give the element and id attribute
$.post(url, { abc: input.val(), xyz: $('#Second_XYZ').val() }, function(response) {
var isValid = response === true || response === "true";
if (isValid) {
input.addClass('valid').removeClass('input-validation-error');
message.empty().addClass('field-validation-valid').removeClass('field-validation-error');
} else {
input.addClass('input-validation-error').removeClass('valid');
message.text(response).addClass('field-validation-error').removeClass('field-validation-valid');
}
})
});
where the controller method would be
[HttpPost]
public ActionResult Validate(string abc, string xyz)
{
bool isValid = .... // code to validate
if (isValid)
{
return Json(true, JsonRequestBehaviour.AllowGet);
}
else
{
return Json("your error message", JsonRequestBehaviour.AllowGet)
}
}

.NET Core Blazor App: How to pass data between pages?

I just started learning how to make websites with using Blazor template. But I don't know how to pass the data from one page to another. It is a bit different than .NET CORE MVC web application and I couldn't find an example for this.
<p>Solve This problem: #rnd1 * #rnd2 = ?</p>
<input type="text" name="result" bind="#result" />
<input type="button" onclick="#calculate" value="Submit" />
I want to send the value in my textbox to the another page. How can I do this?
You can pass it as a parameter.
In the page you want to navigate to, add the parameter to your route:
#page "/navigatetopage/{myvalue}"
and make sure the parameter exists in that page:
[Parameter]
private string myvalue{ get; set; }
In the same page you can pick that up in:
protected override void OnParametersSet()
{
//the param will be set now
var test = myvalue;
}
Now in your start page make sure to navigate to the second page including the value:
uriHelper.NavigateTo($"/navigatetopage/{result}");
That uriHelper needs to be injected like this:
#inject Microsoft.AspNetCore.Blazor.Services.IUriHelper uriHelper
UPDATE PREVIEW-9
on preview-9 you should use navigationManager instead of uriHelper, it also has a NavigateTo method
#inject Microsoft.AspNetCore.Components.NavigationManager navigationManager
More recent versions support the following:
The page you are navigating from:
<NavLink href="/somepage/1">Navigate to page 1</NavLink>
The page you are navigating to:
#page "/somepage/{childId:int}"
<h1>Some Page</h1>
<p role="status">Current child: #childId</p>
#code {
[Parameter]
public int childId { get; set; }
}
I personally prefer to add query string to the url.
For example when I want to pre-select tab when page is loaded:
Call the url like http://localhost:5000/profile?TabIndex=2
In your code you can parse this using NavigationManager and QueryHelpers
Add using Microsoft.AspNetCore.WebUtilities;
Then override one of the lifecycle methods and parse the query parameter
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
Uri uri = this.Nav.ToAbsoluteUri(this.Nav.Uri);
if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("TabIndex", out StringValues values))
{
if (values.SafeAny())
{
_ = int.TryParse(values.First(), out int index);
this.TabIndex = index;
}
}
}
}
I got an error while testing Flores answer in the page were we passing a data
Below is the current Page
#inject Microsoft.AspNetCore.Components.NavigationManager navigationManager
Int64 Id {get;set;}
<MudMenuItem #onclick="#(() => NextPage(#Id))">Next Page</MudMenuItem>
//So Here I am passing the ID which is long
private void NextPage(Int64 Id){
navigationManager.NavigateTo($"/secondPage/{Id}");
}
Second Page
Instead of using -Id only you need to cast it to long or else it throws an error
-From
#page "/pettyCashAuditTrail/{Id}"
-To
#page "/pettyCashAuditTrail/{Id:long}"
[Parameter] public Int64 Id{ get; set; }

Model binding with a child object

I have a class:
public class Application
{
....
public Deployment NewDeployment { get; set; }
....
}
I have an editor template for Deployment within the Application View folder.
The ApplicationViewModel has a SelectedApplication (of type Application), in my Index.cshtml where I use ApplicationViewModel as my Model, I have this call:
#using (Html.BeginForm("Create", "Deployment", new { #id = Model.SelectedId,
q = Model.Query }, FormMethod.Post, new { id = "form", role = "form" }))
{
#Html.EditorFor(m => m.SelectedApplication.NewDeployment)
}
Which then correctly renders out the control in my DisplayTemplates\Deployment.cshtml (though, it may just be pulling the display code and nothing in relation to the NewDeployment object's contents). All is well in the world until I go to submit. At this stage everything seems good. Controller looks like:
public class DeploymentController : Controller
{
[HttpPost]
public ActionResult Create(Deployment NewDeployment)
{
Deployment.CreateDeployment(NewDeployment);
return Redirect("/Application" + Request.Url.Query);
}
}
However, when it goes to DeploymentController -> Create, the object has nulls for values. If I move the NewDeployment object to ApplicationViewModel, it works fine with:
#Html.EditorFor(m => m.NewDeployment)
I looked at the output name/id which was basically SelectedApplication_NewDeployment, but unfortunately changing the Create signature to similar didn't improve the results. Is it possible to model bind to a child object and if so, how?
Your POST action should accept the same model your form is working with, i.e.:
[HttpPost]
public ActionResult Create(ApplicationViewModel model)
Then, you'll be able to get at the deployment the same way as you did in the view:
model.SelectedApplication.NewDeployment
It was technically an accident that using #Html.EditorFor(m => m.NewDeployment) worked. The only reason it did is because the action accepted a parameter named NewDeployment. If the parameter had been named anything else, like just deployment. It would have also failed.
Per Stephen Muecke's comment and with slight modifications, I was able to find how to correct it:
[HttpPost]
public ActionResult Create ([Bind(Prefix="SelectedApplication.NewDeployment")] Deployment deployment)
{
// do things
}

IEnumerable in my ViewModel is not displayed with EditorForModel

ViewModel
[Validator(typeof(ProdutoCategoriaValidator))]
public class ProdutoCategoriaViewModel
{
[HiddenInput(DisplayValue = false)]
public Guid ID { get; set; }
public IEnumerable<SelectListItem> Tipos { get; set; } // <<<<------- Is not showing in my view
[AdditionalMetadata("data-bind", "event: { change: function(data) { Link(data.Nome()); }}")]
public string Nome { get; set; }
[DataType(DataType.Url)]
[AdditionalMetadata("Prefixo", "Produtos/{tipo-de-produto}#")]
public string Link { get; set; }
public int? Ordem { get; set; }
public ProdutoCategoriaViewModel()
{
ID = Guid.NewGuid();
}
}
Solution
View (_Formulario.cshtml)
#model ProdutoCategoriaViewModel
#using (Html.BeginForm(null, null, FormMethod.Post, new { id="form-produtocategoria", data_bind = "submit: salvar" }))
{
#Html.AntiForgeryToken()
<legend>#Html.MvcSiteMap().SiteMapTitle()</legend>
<fieldset>
#Html.ValidationSummary(false, "Verifique os erros abaixo:")
#Html.EditorForModel()
</fieldset>
<div class="buttons">
#Html.ActionLink("Cancelar", "Index")
<input type="submit" value="SALVAR" />
</div>
}
SelectListItem.cshtml
#model IEnumerable<SelectListItem>
#Html.DropDownListFor(m => m, Model)
<p>Test</p>
Result
Full image: http://i.imgur.com/I7HxA.png
Notes
I've tried to put the attribute "UIHint" but still nothing is displayed!
Questions
What am I doing wrong?
By default when you use Html.EditorForModel don't expect this to recurse down to complex properties such as your Tipos property which is of type IEnumerable<SelectListItem>. Brad Wilson explained this in his blog post (more specifically read the Shallow Dive vs. Deep Dive section towards the end of the post). You will need to write a custom editor template for the Object type if you want this to happen.
Another possibility is to specify the template name:
#Html.EditorFor(x => x.Tipos, "SelectListItem")
Also bear in mind that your editor template for the SelectListItem is wrong because you are binding the DropDownListFor to the model as first argument. Don't forget that the first argument of this helper must be a scalar property that will be used to hold the selected value. You need a string or integer property on your view model for this. The second argument represents the collection.
Another important aspect about editor templates is that when you have a property of type IEnumerable<T> and an editor template called T.cshtml this editor template must be strongly typed to the T class and not IEnumerable<T> as you did with your SelectListItem.cshtml template. This doesn't apply if you use UIHint or specify the template name as second argument to the EditorFor helper. n this case the template will be typed to the collection.
So to recap, you could either implement a custom object editor template as Brad Wilson suggested that will recurse down to complex properties or you could modify your _Formulario.cshtml view to specify EditorFor each individual elements.
A #foreach loop renders something that looks right, but the resulting markup will have the same id for each row's controls. It also will not post the enumerable collection back with the model instance.
There are two ways to make this work such that you have a unique id for each item in the collection, and so that the collection is hydrated on postbacks:
1. Use the default editor template rather than a named one
// editor name parameter must be omitted; default editor template's model type
// should be a single instance of the child object, not an IEnumerable. This
// convention looks wrong, but it fully works:
#Html.EditorFor(parentObject => parentObject.Items)
2. Use a #for loop, not a #foreach:
#for (int i = 0; i < parentObject.Items.Count ; i++) {
// the model binder uses the indexer to disambiguate the collection items' controls:
#Html.EditorFor(c => Model.Items[i], "MyEditorTemplate")
}
This will not work, however:
// this will error out; the model type will not match the view's at runtime:
#Html.EditorFor(parentObject => parentObject.Items, "MyEditorTemplate")
Nor will this:
#foreach(var item in parentObject.Items) {
// this will render, but won't post the collection items back with the model instance:
#Html.EditorFor(c => item, "MyEditorTemplate")
}
For a detailed answer why this is, look at this question: MVC can't override EditorTemplate name when used in EditorFor for child object.