Search code examples
c#mvvmdata-bindingblazor

Data binding in Blazor: How to propagate values out of a component?


I have a fairly simple component, which looks like this:

<form>
    <div class="form-group">
        <label for="sampleText">Sample Text</label>
        <input type="text"
               class="form-control"
               id="sampleText"
               aria-describedby="sampleTextHelp"
               placeholder="Enter a text"
               @bind="@Value" />
        <small id="sampleTextHelp" class="form-text text-muted">Enter a text to test the databinding</small>
    </div>
</form>
<p>You entered here: @Value</p>

@code {

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

I now add this to a page like this:

<MVVMTest Value="@_value" />
<p>
    You entered in MVVMTest:
    @_value
</p>

@functions {
    private string _value;
}

When I enter text in the input field, it is correctly updated at You entered here: but not propagated to *You entered in MVVMTest":

MVVM not working

What do I have to do to get this correctly propagated?

I could think of hooking up an Action<string> as a second [Parameter] which I fire inside the component when the text is changed, but it seems like a hackish and roundabout way. Is it how it has to be done, or is there a better way?

Followup 1

The answer from Issac does not work because it stumbles over @bind-value:oninput="@((e) => ValueChanged.Invoke(e.Value))" with Cannot convert lambda expression to type 'object' because it is not a delegate type.

I had to do it in this roundabout way:

<form>
    <div class="form-group">
        <label for="sampleText">Sample Text</label>
        <input type="text"
               class="form-control"
               id="sampleText"
               aria-describedby="sampleTextHelp"
               placeholder="Enter a text"
               @bind="@Value" />
        <small id="sampleTextHelp" class="form-text text-muted">Enter a text to test the databinding</small>
    </div>
</form>
<p>You entered here: @Value</p>

@code {

    private string _value;

    [Parameter]
    public string Value {
        get => _value;
        set {
            if(Equals(value, _value)) {
                return;
            }
            _value = value;
            OnValueChanged.InvokeAsync(value).GetAwaiter().GetResult();
        }
    }

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

And using it like:

@inject HttpClient http
@page "/test"
<div class="top-row px-4">
    Test Page
</div>

<div class="content px-4">
    <MVVMTest Value="@_value" OnValueChanged="@(v => _value = v)" />
    <p>
        You entered in MVVMTest:
        @_value
    </p>
</div>

@functions {
    private string _value;
}

Not pretty, but it works. My "real" component is more complicated as a changed value triggers calls to the underlying ASP.net Core Service so I have to do elaborate detection who changes what to avoid infinite loops.

It would surely be better if Blazor supported XAML/WPF-like MVVM though...

Followup 2

I came back to Issac's solution and with an extra cast, I got it working:

<form>
    <div class="form-group">
        <label for="sampleText">Sample Text</label>
        <input type="text"
               class="form-control"
               id="sampleText"
               aria-describedby="sampleTextHelp"
               placeholder="Enter a text"
               value="@Value"
               oninput="@((Func<ChangeEventArgs, Task>)(async e => await OnValueChanged.InvokeAsync(e.Value as string)))" />
        <small id="sampleTextHelp" class="form-text text-muted">Enter a text to test the databinding</small>
    </div>
</form>
<p>You entered here: @Value</p>

@code {

    private string _value;

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

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

Usage:

<MVVMTest Value="@_value" OnValueChanged="@(v => _value = v)" />
<p>
    You entered in MVVMTest:
    @_value
</p>

@functions {
    private string _value;
}

This is already a known issue in Blazor. I tripped over this last week already, and I posted the workaround in the VS developer forum as well.


Solution

  • You need to add an EventCallback parameter to do that:

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

    read the section Binding with component parameters.