OnChange for whole page: Blazor - c#

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/

Related

Blazored.FluentValidation: Validation won't work if I assign a new class instance to a form model

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

Is there a way to make disabled HTML elements not register Blazor events?

I have found a repetitive pattern in Blazor components when needing to disable an HTML element.
Blazor provides functionality with the HTML disabled attribute, to which you can pass some Razor.
With the example of a single button, we can do:
<button type="button" #onclick="DoSomething" disabled="#buttonIsDisabled">Click me</button>
Blazor will render the button including a disabled attribute when buttonIsDisabled is true. This helps when using a CSS class to alter the styling of the button when disabled as well.
However, this is not very safe since a user can edit the HTML document quickly to remove the disabled attribute from the HTML element. The on-click event would remain and allow the DoSomething method to execute anyways.
To avoid things being bypassed, I use a manual approach instead:
if (buttonIsDisabled)
{
<button disabled type="button" />
}
else
{
<button #onclick="DoSomething" type="button" />
}
There is an enabled version of the button with an onclick handler, and another without it for safety (disabled).
The problem is that in more complex scenarios, is not a simple button which has to be rendered, but multiple elements with multiple handlers as well. This ends up in a pattern of repetitive/duplicated HTML code.
Is there a better approach or another way to disable elements so that Blazor does not register the event handlers such the onclick event?
For example if Blazor knew that an element should render the disabled attribute should not trigger any Blazor event.
Edit:
This question is not intended to find a solution for the case of a disabling/enabling a simple button and controlling it's event handler, but to the pattern I explain above. Imagine having not only disabled buttons, but also inputs, checkboxes, toggles, etc; they could all be also linked to multiple event handlers, each.
A not-supported idea: As Blazor computes the disabled attribute to render it or not, it could also determine to not include the event handlers for the element when it should be disabled in the next rendering.
Applying one of the most basic good coding practices - DRY - Don't Repeat Yourself, here's a simple "button" component:
#if (Show)
{
#if (this.Disabled)
{
<button type="button" disabled #attributes=this.UserAttributes>#ChildContent</button>
}
else
{
<button type="button" #onclick="this.OnClick" #attributes=this.UserAttributes>#ChildContent</button>
}
}
#code {
[Parameter(CaptureUnmatchedValues = true)] public IDictionary<string, object> UserAttributes { get; set; } = new Dictionary<string, object>();
[Parameter] public EventCallback<MouseEventArgs> OnClick { get; set; }
[Parameter, EditorRequired] public RenderFragment? ChildContent { get; set; }
[Parameter] public bool Disabled { get; set; }
[Parameter] public bool Show { get; set; } = true;
}
Which you can then use like this:
#page "/"
<h3>Test</h3>
<MyButton class="btn btn-dark" Disabled=this.disabled Show=this.show OnClick=this.OnClick>Click me</MyButton>
<MyButton class="btn btn-primary" OnClick=this.DisableMe>Disable me</MyButton>
<MyButton class="btn btn-outline-info" OnClick=this.ShowMe>Show me</MyButton>
#code {
private bool disabled;
private bool show = true;
private void DisableMe(MouseEventArgs e)
=> disabled = !disabled;
private void ShowMe(MouseEventArgs e)
=> show = !show;
private void OnClick(MouseEventArgs e)
{
// Do something
}
}
You can use this same pattern for your more complex stuff. Personally, I have a base component (directly implemented as a class) that implements the basics and then derived classes for the more complex stuff
[Polite] Please don't take this the wrong way, but I'm amazed how often DRY goes out the window when writing Razor code!

Blazor EditForm DataAnnotationsValidator validates unexpectedly when a custom child component changes state

My EditForm with a DataAnnotationsValidator contains a custom component that can modify its own state. The trivial code below provides a repro.
When the button in MyComponent is clicked, validation fires on the form. This occurs even when none of the form fields have been modified, which is unexpected and undesirable.
Is this expected behaviour? If so, how can I avoid the validation being triggered by a change to the child component?
This is occurring in a Blazor Server project running .NET 6.0.3.
Index.razor
#page "/"
#using System.ComponentModel.DataAnnotations
<EditForm Model="model">
<DataAnnotationsValidator />
<ValidationSummary />
<label>
Name:
<InputText #bind-Value="model.Name" />
</label>
<MyComponent />
</EditForm>
#code {
MyModel model = new();
class MyModel
{
[Required]
public string? Name { get; set; }
}
}
MyComponent.razor
<p>
MyComponent: #(Switch ? "ON" : "OFF")
<button #onclick="() => Switch = !Switch">Toggle</button>
</p>
#code {
bool Switch = false;
}
In your MyComponent, set the type to button:
<button type="button" #onclick="() => Switch = !Switch">Toggle</button>
For most (you can interpret this as meaning pretty much "all") browsers the default type of button is submit. In Blazor, validation is triggered when a form is submitted, in other words when a button in the EditForm with a type of submit is clicked. This is why you need to set the type to button.
I recommend reading the following article about the type attribute on buttons for some more background.

How to load a non-routable component in Blazer

I work on a project that uses Blazer for the front-end and Cosmos DB in the back-end. I am looping through a list of items and have a button/ anchor tag that OnClick receives a parameter. Currently, I am using the query string parameters to redirect the user from one page to another page. However, I do not want to use query string. I want to make the component non-routable and load it on a button click. Is there a way to pass the parameter on button click and load a non-routable component?
Thank you very much for your explanation, I am using .net 5. Here I am going to write a sample code, so you could help.
My project uses Azure Cosmos DB and Blazor Server.
<!-- begin snippet: js hide: false console: true babel: false -->
<!-- language: lang-html -->
///////////////////////////
MoviesListComponent.razor
////////////////////////
#page "/movies"
foreach(movie in ListOfMovies){
<div> movie.ReleaseeDate</div>
<div> movie.Country</div>
<div> movie.Title</div>
<button type="button" #onClick="()=>LoadAnotherComponent(movie.title)"> on button click redirect to(or load) a non-routable compnent and send movie.title as parameter for example MovieTitleAnalysis component</button>
}
#code{
public List<Movie> ListOfMovies {get;set;}
}
on button click inside MoviesListComponent load
MovieTitleAnalysis.razor and pass the parameter. Dispose MovieListComponent
///////////////////////
MovieTitleAnalysis.razor is a non routable component
//////////////////
<div>welcome to movie Title analysis</div>
#code{
[paramter]
public string movieTitle {get;set;}
void PreformSomeOpration(){}
}
As this is conceptual - you have provided no code context - this should provide a good starting point and demo the basic concept. Comment on how well/poorly it fits!
I've pointed to the "routed" components but it will work with any component.
DynamicPage.razor
#if (ComponentType != null)
{
<DynamicComponent Parameters=this.Parameters Type=this.ComponentType />
}
else
{
#this.ChildContent
}
#code {
[Parameter] public Type? ComponentType { get; set; }
[Parameter] public RenderFragment? ChildContent { get; set; }
[Parameter] public IDictionary<string, object>? Parameters { get; set; }
}
And Index.razor
#page "/"
#*Example Menu*#
<div class="p-2 m-2">
<button class="btn btn-primary" #onclick="() => this.ChangePage(null)">Change to Index</button>
<button class="btn btn-secondary" #onclick="() => this.ChangePage(typeof(Counter))">Change to Counter</button>
<button class="btn btn-info" #onclick="() => this.ChangePage(typeof(FetchData))">Change to FetchData</button>
</div>
<DynamicPage ComponentType=this.page>
<PageTitle>Index</PageTitle>
<div>Hello World from Blazor</div>
</DynamicPage>
#code {
private Type? page = null;
private void ChangePage(Type? newPage)
=> this.page = newPage;
}

Two way data-binding inside blazor template

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);
}

Categories

Resources