26

I'm seeing this common pattern in some libraries (MatBlazor, Telerik) of having ValueChanged and ValueExpression properties and it really confuses me.

What is the difference between both? And when to use it?

Vencovsky
  • 28,550
  • 17
  • 109
  • 176

2 Answers2

31

I would like to add a few use cases for ValueChanged and ValueExpression,

First of all, as enet said, these properties are more like a trinity of properties where you have Foo, FooChanged and FooExpression and it's used in the two-way data bind e.g. @bind-Foo="SomeProperty".

To create a custom component with a property that can be used with @bind- you need to provide these 3 properties (only providing Foo and FooChanged also work) as [Parameter] and call FooChanged when the property inside your custom component changes.

e.g. from enet

[Parameter]
public TValue Foo
{
    get => text
    set
    {
        if (text != value) {
            text = value;
            if (FooChanged.HasDelegate)
            {
                FooChanged.InvokeAsync(value);
            }
        }
    }
}

[Parameter]
public EventCallback<TValue> FooChanged { get; set; }

[Parameter]
public Expression<Func<TValue>> FooExpression { get; set; }  

Adding the @bind-Foo would be the same as passing Value and ValueChanged, the only difference is that @bind- will only set the property, but if you add your own ValueChanged, you can do anything you want (Validating, Changing the value to set, etc).

Use cases

1 - Creating a component that wraps another component with @bind-

If you have an component that already have a @bind-Foo and you want to create a component on top of that and still pass as parameter @bind-Foo, you can have only one property and pass to @bind-Foo, you need to pass properties to Foo, FooChanged and/or FooExpression.

e.g.

CustomInputWrapper.razor

<div>
    <p>My custom input wrapper</p>
    @* If you pass @bind-Value it won't work*@
    @* You need to pass the properties that are used in the bind*@
    <InputText Text="@Value" TextChanged="@ValueChanged" TextExpression="@ValueExpression" />
</div>

@code {    
    [Parameter]
    public virtual string Value { get; set; }

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

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

These situation of wrapping another component will happen a lot if you are making a lot of custom components or don't want to use directly some third party component.

Example of my project: In my project I'm using MatBlazor and Telerik, but not all of the components in both libraries are completely stable, so I created a wrapper around all of the components and one day, when one of these libraries is completely stable, I will change to use only one library. Doing this allow me to have my custom components and if I want to change one, I only change one thing In my custom component and changes the whole application.

2 - Adding default value

If you want to have a default value inside a custom component, you "can" just pass a default value to the property.

[Parameter]
public virtual DateTime Value { get; set; } = new DateTime(/* some default value*/);

But this have a big problem if you use this component inside a form.

Why? Because you will only change the value inside your component, but if a property is passed in @bind-Value it won't be changed.

To add this default value and make it work in the two-way data bind, you need to call ValueChanged and pass the default value. This will make your component have the default value and will also change any property in @bind-Value to have the default value.

e.g.

// Lifecycle after all parameters are set
protected override void OnParametersSet()
{
    // Check if the ValueChanged is set
    if (ValueChanged.HasDelegate)
    {
        ValueChanged.InvokeAsync(DateTime.Now);
    }
}

3 - Use case where you really need FooExpression

When you have an nullable type, e.g. int?, sometimes, when the value is null, it can't know it's type, so you need to pass FooExpression so it can get the type by reflection. Here is an example where you need to use it.


The use case of these properties will be used more if you are making custom components and have to work with binded property or change on how the bind will work.

If you are only using already made components, it will be rare the cases where you will have to use it.

Vencovsky
  • 28,550
  • 17
  • 109
  • 176
  • There are some issues with your answer you should consider before you accept your own answer. As for instance, executing ` ValueChanged.InvokeAsync(DateTime.Now);` from the `OnParametersSet` will surely result in OutOfMemoryException Exception... Also, why do you check if `ValueChanged` has a delegate? The flow of your code will never reach the `OnParametersSet` method if `ValueChanged` contains no delegate. Do please see how the binding is created, and realize that the compiler won't allow your code to compile any further before you set the binding correctly, – enet Oct 21 '22 at 14:52
  • thus making the checking unnecessary and rather misleading. It's really not clear why you use the `OnParametersSet` method in your answer at all. Your parameter `TValue Foo` is badly defined, badly used, and will never work. It also modify a parameter value from within the component... Component parameters should not be modified by the component that get them. See Steve Sanderson commenting about changing a parameter value from within the recieving component. – enet Oct 21 '22 at 14:52
  • `When you have an nullable type, e.g. int?, sometimes, when the value is null, it can't know it's type, so you need to pass FooExpression so it can get the type by reflection.` Wrong... `ValueExpression` has got nothing to do with nullability of variables. Since you're not using the `@bind-Value` compiler instruction, you need to tell the compiler what is the bound value, and that is done by providing the `ValueExpression.` If you use the `@bind-Value`, the compiler provides the `ValueExpression` automatically. – enet Oct 21 '22 at 20:49
22

Actually, you've forgotten the third element of this pattern: Value. This "trinity" of properties is frequently used for component two-way data binding. Notably, these properties are employed inside the built-in Blazor form components, such as <InputText>.

Let's look at an example:

<InputText @bind-Value="employee.FirstName" />
  1. Value is a property provided in the form of @bind-Value="model.PropertyName".

  2. ValueChanged is of type EventCallback<TValue>. It stands for a callback that updates the bound value. As you can see, we do not use it in the above example—it's not necessary. The compiler knows its job and it takes care of this, meaning that it adds an EventCallback "delegate" with all the necessary settings behind your back.

  3. ValueExpression, finally, refers to an expression that identifies the bound value. It is automatically created by the compiler, and you rarely, if ever, have to set it.

Now let's compare the above with the code below. The following example creates a two-way data binding between a parent component and a child component. However, instead of using the standard "trinity" (Value, ValueChanged, ValueExpression), we will replicate the underlying pattern for ourselves:

ParentComponent.razor:

<ChildComponent @bind-Text="FirstName" />

@code {
    [Parameter]
    public string FirstName { get; set; }
}

ChildComponent.razor: (.Net 5.0)

<input type="text" value="@Text" @onchange="OnTextChanged" />

@code 
{
    [Parameter]
    public string Text { get; set; }
    [Parameter]
    public EventCallback<string> TextChanged { get; set; }

    private async Task OnTextChanged(ChangeEventArgs args)
    { 
        await TextChanged.InvokeAsync(args.Value.ToString());
    }
}

The built-in <InputText> and our custom <ChildComponent> are basically the same!


To answer your other question...

When will I use ValueChanged and ValueExpression in Blazor?? I'm creating a wrapper of an input from another library, is this a case for using this trinity?

As explained above, ValueChanged and ValueExpression are properties defined in Blazor's built-in components, and most of the time you won't need to use them directly.

Look again at the two components I've defined above: <ParentComponent> and <ChildComponent>. Change Text and TextChanged to Value and ValueChanged, and my components are still valid and work correctly. The only difference is in naming. What do I do in the <ChildComponent>? I define a parameter property named Text (stands for Value). As I want to enable two-way data binding between the parent and child components, I also need to define a parameter property called here TextChanged (stands for ValueChanged). Text goes to TextChanged, Value goes to ValueChanged, and Year goes to YearChanged. The naming is only convention. The main point is that you have to define a property and an EventCallback of the same data type as the property.

Inside the parent component I provide the property as follows:

<ChildComponent @bind-Text="NameOfAPropertyDefinedInTheParentComponent" /> or <ChildComponent @bind-Value="NameOfAPropertyDefinedInTheParentComponent" /> or <ChildComponent @bind-Year="NameOfAPropertyDefinedInTheParentComponent" />

In my components above, there is also code, as for instance in the child component, that invokes the TextChanged delegate in order to pass a value back to the parent component; this is exactly what the ValueChanged delegate does in the components in which it is defined. But you as a user do not have to use it. Look at my components... They work perfectly well. No need to touch. If you as a user of my component want to subclass it, then you need to know what you're doing and how to subclass a Blazor component properly. But my components, partially presented here, are relatively simple.

Suppose you want to create a password input based on <InputText>, which is not only doable but quite easy. In that case, you're not going to change anything but the look of the <InputText> component so that asterisk symbols are displayed instead of normal text. The rest of the component is unchanged. You do not need to handle the events and such. This, of course, does not mean that a component author will never need to call the EventCallback from somewhere in his code. That said, I have never had a good reason to trigger the ValueChanged delegate when using the <InputText> component. And I only once had to provide a ValueExpression, as the compiler was not able to identify the bound value. (I'll look for it, and if found I'll post it here...)

enet
  • 41,195
  • 5
  • 76
  • 113
  • When will I use `ValueChanged` and `ValueExpression` in Blazor?? I'm creating a wrapper of an input from another library, is this a case for using this trinity? – Vencovsky Mar 12 '20 at 18:21
  • 3
    Great answer, this explains indirectly that it is a Blazor convention and MatBlazor and Telerik are following convention set by the framework. – Ed Charbeneau Mar 12 '20 at 18:59
  • @Ed Charbeneau, thanks for your compliments... I'm flattered, as I've learned a great deal from your work... – enet Mar 12 '20 at 19:39
  • Incidentally, mentioning Telerik, I now remember it was the only place I saw a sample where the ValueExpression was dealt with and explained. Than you. – enet Mar 12 '20 at 19:41
  • That's awesome. Glad to hear you found my work helpful. – Ed Charbeneau Mar 13 '20 at 00:12
  • 3
    @Vencovsky As far as I understand it, whenever you implement Components that make use of two-way binding, you should use `Value` and `ValueChanged`. If you use functionality that depends on identifying the bound property (eg. implementing Validation). Then you also need to use `ValueExpression`. For example you can create a `FieldIdentifier` using `FieldIdentifier.Create(ValueExpression)`. - Hope this helps. – LuckyLikey Feb 16 '21 at 12:43
  • `InvokeAsync` is an `async void` "fire-and-forget" - is that the typical approach in these cases, and is "safe"? – lonix Oct 21 '22 at 11:18
  • 1
    @lonix, the truth is `fire-and-forget` should be only used as a last resort... Note that I've changed my code to reflect the correct way to create the binding, based on experience, and a Github comment by Steve Sanderson, the creator of Blazor, that the child component should not modify a parameter value passed to it. In my changed code, I do not modify the value of the Text property in the child component... Instead, I pass it to the parent component, after which the child component is re-rendered with the that value passed to it from the parent. – enet Oct 21 '22 at 13:38