I would like to use a two-way databinding between a component inside a template and its parents. The one way data binding is working well. But when I modified the Context passed from the RenderFragment this modification is not propagate to the template (container). Here is the example.
This it the template definition. We have a form and we want to be able to specify the content of the form in function of the model.
#typeparam TItem
#typeparam TItemDtoCU
#typeparam TDataService
<EditForm Model="#Item" OnValidSubmit="HandleValidSubmit" class="item-editor">
<DataAnnotationsValidator />
#FormContentTemplate(Item)
<span class="save">
<MatButton Type="submit" Raised="true">Save</MatButton>
<MatButton Type="reset" Raised="true" OnClick="HandleCancelSubmit">Cancel</MatButton>
</span>
</EditForm>
Here is the place where I use this template
<ModelEditorTemplate TItem="ClassName"
TItemDtoCU="OtherClassName"
TDataService="ServiceClassName"
ItemId="SelectedItem?.Id"
OnItemAdded="ItemAdded"
OnItemUpdated="ItemUpdated">
<FormContentTemplate>
<span>
<MatTextField Label="Name" #bind-Value="context.Name" #bind-Value:event="onchange" />
<ValidationMessage For="#(() => context.Name)" />
</span>
</FormContentTemplate>
</ModelEditorTemplate>
When the user modified the MatTextField field is reset to the initial value provided by the template component.
Do you have an idea ?
Edit 1 : More information about the way we fetched the data
Yes the TDataService is fetching the component from a REST Api. Here the partial class linked to the template :
public partial class ModelEditorTemplate<TItem, TItemDtoCU, TDataService> : ParentThatInheritsFromComponentBase
where TItem : BaseEntity, new()
where TDataService : ICrudService<TItem, TItemDtoCU>
{
public TItem Item = new TItem();
[Inject]
protected TDataService DataService { get; set; }
protected override async Task OnParametersSetAsync()
{
//...
await LoadItem();
// ...
}
protected async Task LoadItem()
{
//...
Item = await DataService.Get(ItemId.Value);
// ....
}
}
When the field inside FormContentTemplate RenderFragment update the model, it triggers the OnParametersSetAsync method of the template ,which fetch again the data from the server, update the model and then overwrite the modification done by the user.
There was no issue, with the form input inside the template just bad understanding of how it works.
I corrected the problem by using the SetParametersAsync to check if the parameter that change was the one, I was looking for and not the RenderFragment. The ItemId is the id of the record, I fetch from the DataService.
public override async Task SetParametersAsync(ParameterView parameters)
{
_hasIdParameterChange = false;
if(parameters.TryGetValue(nameof(ItemId), out int? newItemIdValue))
{
_hasIdParameterChange = (newItemIdValue != ItemId);
}
await base.SetParametersAsync(parameters);
}
Related
To add some context, I'm trying to create a Dropdown select Blazor component. I've managed to create a concept of this entirely with CSS, #onclick, and #onfocusout.
I'm trying to pass a reference of the DropDown component to its children, DropDownItem. The only way I know how to achieve this, is by using the #ref and passing it as a parameter to the DropDownItem component.
<DropDown #ref="DropDownReference">
<DropDownItem ParentDropDown=#DropDownReference>Hello</DropDownItem>
<DropDownItem ParentDropDown=#DropDownReference>World</DropDownItem>
</DropDown>
There has to be a cleaner approach here that does not require manually passing the reference down to each child instance. I suppose I could use CascadingValue but that will still require me to store the DropDown reference.
I'm trying to notify DropDown parent when a click event occurs in DropDownItem. This will signal the parent to changes it selected value - as it would traditionally work in a select.
Here is an example of how you could do it using CascadingValue. The DropDownItem component will accept a [CascadingParameter] of type DropDown. There is nothing wrong in doing that, this is how it's done in most (if not all) component libraries.
DropDown.razor
<CascadingValue Value="this" IsFixed="true">
#* Dropdown code *#
<div class="dropdown">
#ChildContent
</div>
</CascadingValue>
#code {
[Parameter] public RenderFragment ChildContent { get; set; }
private string selectedItem;
public void SelectItem(string item)
{
selectedItem = item;
StateHasChanged();
}
}
DropDownItem.razor
#* Dropdown item code *#
<div class="dropdown-item" #onclick="OnItemClick">...</div>
#code {
[CascadingParameter] public DropDown ParentDropDown { get; set; }
[Parameter] public string Name { get; set; }
private void OnItemClick()
{
ParentDropDown.SelectItem(Name);
}
}
Usage:
<DropDown>
<DropDownItem Name="Hello">Hello</DropDownItem>
<DropDownItem Name="World">World</DropDownItem>
</DropDown>
In my server-side Blazor app I have a TelerikForm, which is a wrapper around Blazor's EditForm. Form's model parameter is "vendor"
For form validation I use Blazored.FluentValidation, which is registered as a Transient service.
Validation works fine if I fill out all form fields manually. But in some scenarios, I need to fill all fields programmatically, assigning a new class instance to the "vendor" model: vendor = newVendor.
In that case, the validator stops working. It seems that the validator doesn't recognize a new model. I think it is bound to the model's reference, and when the model changes its reference validator doesn't work anymore. However, if I assign model properties one by one, then it works fine, eg - vendor.Name = newVendor.Name.
How can I make a validator work with a new instance of the model?
Below is my code:
<TelerikForm Model="#vendor">
<FormValidation>
<FluentValidationValidator #ref="vendorValidator" DisableAssemblyScanning="true" />
</FormValidation>
<FormItems>
<FormItem Field="#nameof(Vendor.TaxId)" />
<FormItem Field="#nameof(Vendor.VendorName)" />
<FormItem Field="#nameof(Vendor.CountryCode)" />
<FormItem Field="#nameof(Vendor.CompanyWebsite)" />
<TelerikValidationSummary />
</FormItems>
<FormButtons>
<TelerikButton OnClick="#(() => ValidateVendor())">Submit</TelerikButton>
</FormButtons>
</TelerikForm>
#code {
Vendor? vendor = new();
FluentValidationValidator? vendorValidator;
bool ValidateVendor()
{
return vendorValidator.Validate(options => options.IncludeAllRuleSets());
}
void FillFormProgrammaticallyAndValidate(Vendor newVendor)
{
vendor = newVendor;
ValidateVendor();
// it works if I assign properties one by one
// vendor.VendorName = newVendor.VendorName;
// vendor.CountryCode = newVendor.CountryCode
}
public class VendorModelValidator : AbstractValidator<Vendor>
{
public VendorModelValidator()
{
RuleSet("ValidateName", () =>
{
RuleFor(p => p.VendorName)
.NotEmpty().WithMessage("'Name' is mandatory");
});
}
}
}
sdsd
TelerikForm is the Telerik Blazor EditForm component, with the same parameters as a regular EditForm.
Technically, you can change model in EditForm and all will run fine if the component is rendered again:
#using System.ComponentModel.DataAnnotations
<EditForm Model="#exampleModel" OnValidSubmit="#HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<InputText id="name" #bind-Value="exampleModel.Name" />
<button type="submit">Submit</button>
</EditForm>
#exampleModel.Name
<button #onclick="ChangeModel">Change model</button>
#code {
public class ExampleModel
{
[Required]
[StringLength(10, ErrorMessage = "Name is too long.")]
public string? Name { get; set; }
}
private ExampleModel exampleModel = new();
private void HandleValidSubmit()
{
}
protected void ChangeModel()
{
exampleModel = new();
// At this point this component is rendered again
}
}
If you take a look to EditForm source code, you can appreciate that is ready to deal with this scenario: "or if they are supplying a different Model":
// Update _editContext if we don't have one yet, or if they are supplying a
// potentially new EditContext, or if they are supplying a different Model
if (Model != null && Model != _editContext?.Model)
{
_editContext = new EditContext(Model!);
}
I don't know if you can do the same with TelerikForm because this is not an open source component. Open a ticket on Telerik and ask for it.
In any case, I recommend to you to avoid this practice and just assign new values to current model instead to change it. In my opinion, you should avoid changing the EditForm's model.
Also, you can notice that I posted a Minimum Reproducible Sample, you can copy-paste it, and it runs without issues. You should to learn about How to create a Minimal, Reproducible Example
I am creating a .net core 5 web application using razor pages and am struggling with binding view components that I have created to my page -- IF I have multiple of the same view component on the page.
The below works perfectly:
MyPage.cshtml:
#page
#model MyPageModel
<form id="f1" method="post" data-ajax="true" data-ajax-method="post">
<vc:my-example composite="Model.MyViewComposite1" />
</form>
MyPage.cshtml.cs
[BindProperties]
public class MyPageModel : PageModel
{
public MyViewComposite MyViewComposite1 { get; set; }
public void OnGet()
{
MyViewComposite1 = new MyViewComposite() { Action = 1 };
}
public async Task<IActionResult> OnPostAsync()
{
// checking on the values of MyViewComposite1 here, all looks good...
// ...
return null;
}
}
MyExampleViewComponent.cs:
public class MyExampleViewComponent : ViewComponent
{
public MyExampleViewComponent() { }
public IViewComponentResult Invoke(MyViewComposite composite)
{
return View("Default", composite);
}
}
Default.cshtml (my view component):
#model MyViewComposite
<select asp-for="Action">
<option value="1">option1</option>
<option value="2">option2</option>
<option value="3">option3</option>
</select>
MyViewComposite.cs
public class MyViewComposite
{
public MyViewComposite() {}
public int Action { get; set; }
}
So up to this point, everything is working great. I have a dropdown, and if I change that dropdown and inspect the value of this.MyViewComposite1 in my OnPostAsync() method, it changes to match what I select.
However, I now want to have MULTIPLE of the same view component on the page. Meaning I now have this:
MyPage.cshtml:
<form id="f1" method="post" data-ajax="true" data-ajax-method="post">
<vc:my-example composite="Model.MyViewComposite1" />
<vc:my-example composite="Model.MyViewComposite2" />
<vc:my-example composite="Model.MyViewComposite3" />
</form>
MyPage.cshtml:
[BindProperties]
public class MyPageModel : PageModel
{
public MyViewComposite MyViewComposite1 { get; set; }
public MyViewComposite MyViewComposite2 { get; set; }
public MyViewComposite MyViewComposite3 { get; set; }
public void OnGet()
{
MyViewComposite1 = new MyViewComposite() { Action = 1 };
MyViewComposite2 = new MyViewComposite() { Action = 1 };
MyViewComposite3 = new MyViewComposite() { Action = 2 };
}
public async Task<IActionResult> OnPostAsync()
{
// checking on the values of the above ViewComposite items here...
// Houston, we have a problem...
// ...
return null;
}
}
I now have three downdowns showing on the page as I would expect, and those three dropdowns are all populated correctly when the page loads. So far so good!
But let's say that I selection "option3" in the first dropdown and submit the form. All of my ViewComposites (MyViewComposite1, MyViewComposite2 and MyViewComposite3) ALL show the same value for Action, even if the dropdowns all have different options selected.
I believe that I see WHY this is happening when I inspect the controls using dev tools:
<select name="Action">...</select>
<select name="Action">...</select>
<select name="Action">...</select>
As you can see, what is rendered is three identical options, all with the same name of "Action". I had hoped that giving them different ids would perhaps help, but that didn't make a difference:
<select name="Action" id="action1">...</select>
<select name="Action" id="action2">...</select>
<select name="Action" id="action3">...</select>
This is obviously a slimmed down version of what I am trying to do, as the view components have a lot more in them than a single dropdown, but this illustrates the problem that I am having...
Is there something I am missing to make this work?
Any help would be greatly appreciated!
The HTML output shows clearly that all the selects have the same name of Action which will cause the issue you are encountering. Each ViewComponent has no knowledge about its parent view model (of the parent view in which it's used). So basically you need somehow to pass that prefix info to each ViewComponent and customize the way name attribute is rendered (by default, it's affected only by using asp-for).
To pass the prefix path, we can take advantage of using ModelExpression for your ViewComponent's parameters. By using that, you can extract both the model value & the path. The prefix path can be shared in the scope of each ViewComponent only by using its ViewData. We need a custom TagHelper to target all elements having asp-for and modify the name attribute by prefixing it with the prefix shared through ViewData.
That will help the final named elements have their name generated correctly, so the model binding will work correctly after all.
Here is the detailed code:
[HtmlTargetElement(Attributes = "asp-for")]
public class NamedElementTagHelper : TagHelper
{
[ViewContext]
[HtmlAttributeNotBound]
public ViewContext ViewContext { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
//get the name-prefix shared through ViewData
//NOTE: this ViewData is specific to each ViewComponent
if(ViewContext.ViewData.TryGetValue("name-prefix", out var namePrefix) &&
!string.IsNullOrEmpty(namePrefix?.ToString()) &&
output.Attributes.TryGetAttribute("name", out var attrValue))
{
//format the new name with prefix
//and set back to the name attribute
var prefixedName = $"{namePrefix}.{attrValue.Value}";
output.Attributes.SetAttribute("name", prefixedName);
}
}
}
You need to modify your ViewComponent to something like this:
public class MyExampleViewComponent : ViewComponent
{
public MyExampleViewComponent() { }
public IViewComponentResult Invoke(ModelExpression composite)
{
if(composite?.Name != null){
//share the name-prefix info through the scope of the current ViewComponent
ViewData["name-prefix"] = composite.Name;
}
return View("Default", composite?.Model);
}
}
Now use it using tag helper syntax (note: the solution here is convenient only when using tag helper syntax with vc:xxx tag helper, the other way of using IViewComponentHelper may require more code to help pass the ModelExpression):
<form id="f1" method="post" data-ajax="true" data-ajax-method="post">
<vc:my-example composite="MyViewComposite1" />
<vc:my-example composite="MyViewComposite2" />
<vc:my-example composite="MyViewComposite3" />
</form>
Note about the change to composite="MyViewComposite1", as before you have composite="Model.MyViewComposite1". That's because the new composite parameter now requires a ModelExpression, not a simple value.
With this solution, now your selects should be rendered like this:
<select name="MyViewComposite1.Action">...</select>
<select name="MyViewComposite2.Action">...</select>
<select name="MyViewComposite3.Action">...</select>
And then the model binding should work correctly.
PS:
Final note about using a custom tag helper (you can search for more), without doing anything, the custom tag helper NamedElementTagHelper won't work. You need to add the tag helper at best in the file _ViewImports.cshtml closest to the scope of where you use it (here your ViewComponent's view files):
#addTagHelper *, [your assembly fullname without quotes]
To confirm that the tag helper NamedElementTagHelper works, you can set a breakpoint in its Process method before running the page containing any elements with asp-for. The code should hit in there if it's working.
UPDATE:
Borrowed from #(Shervin Ivari) about the using of ViewData.TemplateInfo.HtmlFieldPrefix, we can have a much simpler solution and don't need the custom tag helper NamedElementTagHelper at all (although in a more complicated scenario, that solution of using a custom tag helper may be more powerful). So here you don't need that NamedElementTagHelper and update your ViewComponent to this:
public class MyExampleViewComponent : ViewComponent
{
public MyExampleViewComponent() { }
public IViewComponentResult Invoke(ModelExpression composite)
{
if(composite?.Name != null){
ViewData.TemplateInfo.HtmlFieldPrefix = composite.Name;
}
return View("Default", composite?.Model);
}
}
Each component only binds data, base on the defined model so you always have the same name fields in the result. In razor you can pass viewdata to the component.
you should create custom viewdata for your components.
#{
var myViewComposite1VD = new ViewDataDictionary(ViewData);
myViewComposite1VD.TemplateInfo.HtmlFieldPrefix = "MyViewComposite1";
var myViewComposite2VD = new ViewDataDictionary(ViewData);
myViewComposite2VD.TemplateInfo.HtmlFieldPrefix = "MyViewComposite2";
var myViewComposite3VD = new ViewDataDictionary(ViewData);
myViewComposite3VD.TemplateInfo.HtmlFieldPrefix = "MyViewComposite3";
}
<form id="f1" method="post" data-ajax="true" data-ajax-method="post">
<vc:my-example composite="MyViewComposite1" view-data="myViewComposite1VD " />
<vc:my-example composite="MyViewComposite2" view-data="myViewComposite2VD"/>
<vc:my-example composite="MyViewComposite3" view-data="myViewComposite3VD "/>
</form>
As you see you can change the bindings using TemplateInfo.HtmlFieldPrefix
Just thinking about coming up with a templated blazor component to do a CRUD style Single Page App which can have a specific object passed in so I don't have to write the same boilerplate code over and again.
so for instance as below parts of it can be templated by using RenderFragment objects:
#typeparam TItem
<div>
#if (AddObjectTemplate != null)
{
#AddObjectTemplate
}
else
{
<div style="float:left">
<button class="btn btn-primary" #onclick="AddObject">Add Object</button>
</div>
}
</div>
#code {
[Parameter]
public RenderFragment AddObjectTemplate { get; set; }
[Parameter]
public IList<TItem> Items { get; set; }
}
However further down I might want to have something like this:
<button class="btn btn-default" #onclick="#(() => EditObject(item.Id))">Edit</button>
protected void EditObject(int id)
{
TItem cust = _itemServices.Details(id);
}
The issue is that the above call to EditObject(item.Id) cannot resolve to a specific object at this moment because it does not know what TItem is. Is there a way to use a specific interface in the template component that each object must implement or is there another way of doing this?
The idea would be to have AddObject, EditObject, DeleteObject etc which all basically do the same thing but with different types of object.
Since you have the IList<TItem> as a parameter, the list exists at another level of the component structure outside of this component. Because of this you might be better off using the EventCallBack<T> properties for your Add, Edit, and Delete methods, and having the actual methods set as you wire the component up. This makes your template component a rendering object only, and you keep the real "work" to be done close to the actual list that needs the work done.
When you set up your template component, you might try something like this which I've had good results with.
Templator.razor
#typeparam TItem
<h3>Templator</h3>
#foreach (var item in Items)
{
#ItemTemplate(item)
<button #onclick="#(() => EditItemCallBack.InvokeAsync(item))">Edit Item</button>
}
#code {
[Parameter]
public IList<TItem> Items { get; set; }
[Parameter]
public EventCallback<TItem> EditItemCallBack { get; set; }
[Parameter]
public RenderFragment<TItem> ItemTemplate
}
Container.Razor
<h3>Container</h3>
<Templator TItem="Customer" Items="Customers" EditItemCallBack="#EditCustomer">
<ItemTemplate Context="Cust">
<div>#Cust.Name</div>
</ItemTemplate>
</Templator>
#code {
public List<Customer> Customers { get; set; }
void EditCustomer(Customer customer)
{
var customerId = customer.Id;
//Do something here to update the customer
}
}
Customer.cs
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
}
The key points here would be as follows:
The actual root list is living outside the templated component, as are the methods to work on that list, so all of the above are at the same level of abstraction and the same chunk of code.
The template component receives a list, a type to specify what type of the list items will be, and a method callback to execute on each item. (or series of methods, you can add the "Add" and "Delete" methods as you see fit using the same approach). It also receives the <ItemTemplate> render fragment that you specify when calling the code in the Container.razor file.
The 'foreach' in the templated item makes sure each TItem gets set up for it's own RenderFragment, set of buttons and callback functions.
Using the EventCallBack<TItem> as a parameter means that you assign a method to it that expects back the whole object of TItem. This is good, as the template now doesn't care what type the TItem is, only that is has the ability to call a method that takes a TItem as an argument! Handling the instance of whatever TItem is is now the responsibility of the calling code, and you don't have to try to constrain the generic type TItem. (Which I haven't had any luck doing in Blazor yet, maybe future releases)
As for how to render whatever TItem you feed into it, that is explained well in the documentation HERE.
Hope this helps!
I want an alert if a user want to leave the page and doesnt save the changes in blazor.
How can i detect the change with high performance (dont want to check the database)?
In blazor the #bind values updating automatically. I guess there is a service that checked already something changed.
How can i get this information?
You could use the IsModified() from the EditContext:
#if (_editContext.IsModified())
{
<p>You have made changes. Any unsaved changes will be lost!</p>
}
<EditForm EditContext="_editContext" OnValidSubmit="OnValidSumit">
<DataAnnotationsValidator />
<ValidationSummary />
<InputText #bind-Value="Model.Something" />
<button type="submit">Add</button>
</EditForm>
#code {
public Model Model { get; set; } = new Model();
private EditContext _editContext;
protected override void OnInitialized()
{
_editContext = new EditContext(Model);
}
}
For checking if the user wants to navigate to another page, here is a great article: https://chrissainty.com/an-in-depth-look-at-routing-in-blazor/
and: https://blazor-university.com/routing/detecting-navigation-events/