LuckyLikey
LuckyLikey

Reputation: 3850

How to set ValidationMessage<TValue>.For Property dynamically in Blazor?

This Section of the Docs describes, how to display Validation Messages.

<ValidationMessage For="() => Parameters.PropertyA"></ValidationMessage>      

How can the ValidationMessage.For Property be set dynamically?

Since For is of type Expression<Func<TValue>>, I want to pass a Func instead, but this doesn't compile:

[Parameter]
public Func<string> PropertyLocator { get; set; }

<ValidationMessage For="PropertyLocator"></ValidationMessage>

this compiles, but Validation Messages won't be resolved correctly

<ValidationMessage For="() => PropertyLocator"></ValidationMessage>

I also tried to make the Component generic, such that it knows about the Parameters Type:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Components;

public partial class MyComponent<TParam>
{
    [Parameter]
    public TParam Parameters { get; set; }

    [Parameter]
    public Func<TReportParam, string> PropertyLocator { get; set; }
}


@using System.Linq.Expressions
@typeparam TParam
<ValidationMessage For="@((Expression<Func<string>>)(() => PropertyLocator(this.Parameters)))"></ValidationMessage>


<MyComponent TParam="MyParameters" Parameters="BindToSomeValue" PropertyLocator="(parameters) => parameters.PropertyA" />

But this leads to the following run-time exception:

Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100] Unhandled exception rendering component: The provided expression contains a InvocationExpression1 which is not supported. FieldIdentifier only supports simple member accessors (fields, properties) of an object. System.ArgumentException: The provided expression contains a InvocationExpression1 which is not supported. FieldIdentifier only supports simple member accessors (fields, properties) of an object. at Microsoft.AspNetCore.Components.Forms.FieldIdentifier.ParseAccessor[String](Expression`1 accessor, Object& model, String& fieldName) at Microsoft.AspNetCore.Components.Forms.FieldIdentifier.Create[String](Expression`1 accessor) at Microsoft.AspNetCore.Components.Forms.ValidationMessage`1[[System.String, System.Private.CoreLib, Version=5.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].OnParametersSet() at Microsoft.AspNetCore.Components.ComponentBase.CallOnParametersSetAsync() at Microsoft.AspNetCore.Components.ComponentBase.RunInitAndSetParametersAsync()

Upvotes: 4

Views: 2617

Answers (3)

LuckyLikey
LuckyLikey

Reputation: 3850

After some research I stumbled about the following blazor feature:

The holy trinity of blazor bindings

Read more about it here.

In short, if a [Parameter] is bound with the follwoing syntax...

<MyComponent @bind-Value="My.Binding.Path" />

... it not only supports two-way bindings, but it also sets a locator expression.

[Parameter]
public string Value { get; set; }

[Parameter]
public EventCallback<string> ValueChanged { get; set; }

[Parameter]
public Expression<Func<string>> ValueExpression { get; set; }

you may use any type, instead of string

since the value of the ValueExpression is set automatically, you can use this behavior to display the validation message for the bound property. Simply add the ValidationMessage Component to your component with the expression.

<ValidationMessage For="ValueExpression" />

A little extra

If you're building a Component that supports Validation (which at this point, I assume you are). The following might also be interesting for you.

Not only can you use the holy trinity to display validationmessages, but also to create Components supporting validation. There are many articles covering this topic.

In short:

  • Build your component
  • Notify field changes on the EditContext whenever needed

To make the above created MyComponents Value Property support validation, just follow these steps.

Define a CascadingParameter EditContext, this gets the current EditContext, usually from the EditForm Component. Also note that the EditContext may not be set, if there's no CascadingValue. For example if the Component isn't placed inside an EditForm:

[CascadingParameter]
public EditContext? EditContext

Define a property to store a FieldIdentifier and set it when parameters are set.

public FieldIdentifier? FieldIdentifier { get; private set; }

public override async Task SetParametersAsync(ParameterView parameters)
{
    await base.SetParametersAsync(parameters);

    if (this.EditContext != null && this.DateExpression != null && this.FieldIdentifier?.Model != this.EditContext.Model)
    {
        this.FieldIdentifier = Microsoft.AspNetCore.Components.Forms.FieldIdentifier.Create(this.DateExpression);
    }
}

Trigger the validation for the Field whenever you need (usually after the invocation of ValueChanged):

this.Value = value;
this.ValueChanged.InvokeAsync(this.Value);
if (this.FieldIdentifier?.FieldName != null)
{
    this.EditContext?.NotifyFieldChanged(this.FieldIdentifier!.Value);
}

Upvotes: 3

Just the benno
Just the benno

Reputation: 2601

I've created a small sample page.

The model uses DataAnnotations as the validation mechanism.


    public class DemoInputModel
    {
        [Required]
        public String PropertyOne { get; set; }

        [MinLength(2)]
        public String PropertyTwo { get; set; }

        [MaxLength(5)]
        public String PropertyThree { get; set; }
    }

On the page, the model is initialized and set as the edit context. We have three text inputs and a select box. The select box can be used to toggle the validation message. If the value is of the select box is changed, a new expression is assigned to the ValidationMessage.


@using System.ComponentModel.DataAnnotations;
@using System.Linq.Expressions;

@page "/test"


<h1>ValidationMessageTest</h1>

<EditForm Model="_model">
    <DataAnnotationsValidator />

    <ValidationMessage For="ValidationResolver"></ValidationMessage>

    <InputText @bind-Value="_model.PropertyOne" />
    <InputText @bind-Value="_model.PropertyTwo" />
    <InputText @bind-Value="_model.PropertyThree" />

    <InputSelect @bind-Value="SelectedValidationProperty">
        <option value="1">1</option>
        <option value="2">2</option>
        <option value="3">3</option>
    </InputSelect>

    @*<ValidationSummary />*@

</EditForm>


@code {

    private DemoInputModel _model = new DemoInputModel
    {
        PropertyOne = "Test",
        PropertyTwo = "42",
        PropertyThree = "Math.PI",
    };

    private String _selectedValidationProperty;

    public String SelectedValidationProperty
    {
        get => _selectedValidationProperty;
        set
        {
            _selectedValidationProperty = value;
            ChangeValidator(value);
        }
    }

    public Expression<Func<String>> ValidationResolver { get; set; }

    protected override void OnInitialized()
    {
        SelectedValidationProperty = "1";
        base.OnInitialized();
    }

    public void ChangeValidator(String value)
    {
        switch (value)
        {
            case "1":
                ValidationResolver = () => _model.PropertyOne;
                break;
            case "2":
                ValidationResolver = () => _model.PropertyTwo;
                break;
            case "3":
                ValidationResolver = () => _model.PropertyThree;
                break;
            default:
                break;
        }
    }
}

Did you mean something like this? It gets slightly more complicated if your model doesn't have only strings, like in the example. A "quick" workaround could be to have an Expression for each possible type.

Under the hood, the expression is used to create a FieldIdentifier. The FieldIdentifier is then used to get the corresponding property/field from the EditContext to check the validation status. Hence, you are constrained in what to choose for the expression. The error message FieldIdentifier only supports simple member accessors (fields, properties) of an object gives a good indication of this limitation.

Upvotes: 3

Vencovsky
Vencovsky

Reputation: 31693

I want to pass a Func instead

Why? If there isn't a specific reason why you should pass Func<TValue> instead of Expression<Func<TValue>>, just have the parameter

[Parameter]
public Expression<Func<string>> PropertyLocator { get; set; }

If you want only a Func<> because you are going to reuse it for something else other than the For parameter of ValidationMessage, you can take a look at Extracting Func<> from Expression<> to get a Func<> from the Expression<Func<string>> PropertyLocator.

If you really want to pass a Func<>, maybe you will get some problems to transform when trying to convert a .net Func to a .net Expression<Func>.

Upvotes: 1

Related Questions