David Thielen
David Thielen

Reputation: 32996

Is the Value [Parameter] the initial value, never to change?

In my Rating.razor.cs component I have:

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

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

Based on replies from individuals here I respect, a parameter should never have a value assigned to it inside the component. Update per @HH below - The Value is only set by the parent razor file.

And the way for the parent component to get the changed value is by assigning a method to ValueChanged - correct? So the component should have an internal property that stores the new value if it needs to retain the present value - correct?

And... @bind-Value=Model.Rating is a shortcut for:

<Rating Value="Model.Rating" ValueChanged="BlazorCreatedOnValueChanged"/>

private void BlazorCreatedOnValueChanged(int rating) 
    {
    Model.Rating = rating;
    }

Is this correct?


Based on the answers & comments below (as always thank you Bennyboy, HH, & MrC)

First off, read the answers & comments below in full. I'm summarizing here and there's details in the below answers that are not included here.

The way you should set this up, using rating as an example

  1. The rating is passed to the child component using Value=Model.Rating.
  2. The child component renders using this value
  3. The user clicks a star changing the rating.
  4. The child component calls ValueChanged(rating) passing the new rating to the parent.
  5. The parent sets Model.Rating = rating;
  6. Blazor re-renders due to a change in a parameter.
  7. The rating child component reads the value from Value=Model.Rating and renders using this new value.

Notes:

  1. The child component never writes to Value.
  2. The parent never reads the child's `Va;ue".
  3. The parent gets any change via ValueChanged.
  4. @bind-value and bind:set/get are shorthand to the above cycle. To the extent that for the set it creates it's own ValueChanged method in the parent razor to call to update the Model.Rating.
  5. Value is updated and that updated value is used to display the child. But it is updated via the cycle above that triggers a render that causes the Value to be read anew.

Upvotes: 0

Views: 797

Answers (3)

Henk Holterman
Henk Holterman

Reputation: 273621

So does this mean Value is the initial vale for my rating component?

No, of course not.

When this component executes ValueChanged.InvokeAsync(newValue) then the parent component will register that (and maybe validate/limit the value) and then set a new Value with the other branch of the two-way binding. Value always is what the parent says it is.

The "don't write to your own parameters" rule is there to avoid having two conflicting versions of the truth. In this example Model.Rating is leading.

It is a bit confusing because the data-flow takes the long way around:

newValue -> ValueChanged -> Model.Rating -> Render -> OnSetParameters -> Value

Upvotes: 1

MrC aka Shaun Curtis
MrC aka Shaun Curtis

Reputation: 30320

Here, it's vitally important to understand the underlying processes.

Edit events are all driven from the Browser event. The Blazor Client side JS code has a registered handler for the event which calls back though JS Interop into the Hub/Renderer process. This invokes the mapped C# method: in this case that's ValueChanged.

There's no magic "automatic" setting of Value. Value remains at the old value until the parent sets the incoming parameter value, the renderer calls SetParametersAsync on the component, and the ParameterView.SetParameterProperties sets it to the new [Parameter supplied] value.

This process isn't without its problems.

  • If a render occurs before one driven by SetParametersAsync, the component will be rendered using the current [not new] value of Value.

Even Microsoft resorts to a little skulduggery in InputBase to avert this. The code does a small NoNo by temporarily setting Value locally. There's an explanation why attached to the offending line.

    protected TValue? CurrentValue
    {
        get => Value;
        set
        {
            var hasChanged = !EqualityComparer<TValue>.Default.Equals(value, Value);
            if (hasChanged)
            {
                _parsingFailed = false;

                // If we don't do this, then when the user edits from A to B, we'd:
                // - Do a render that changes back to A
                // - Then send the updated value to the parent, which sends the B back to this component
                // - Do another render that changes it to B again
                // The unnecessary reversion from B to A can cause selection to be lost while typing
                // A better solution would be somehow forcing the parent component's render to occur first,
                // but that would involve a complex change in the renderer to keep the render queue sorted
                // by component depth or similar.
                Value = value;

                _ = ValueChanged.InvokeAsync(Value);
                EditContext?.NotifyFieldChanged(FieldIdentifier);
            }
        }
    }
  • If you want to discard the change (maybe it's outside the range of values allowed) and revert to the current value, you need to do a little skulduggery to force the Renderer's diffing engine to recognise the change. There's a mismatch between the value in the browser control [the new one] and the value in the Renderer's DOM [the current one]. If you set it back to the current value [the value in the Renderer's DOM], the diffing engine sees no difference so doesn't pass a change through to the browser side virtual DOM.

Upvotes: 1

Syed Muhammad Ali Raza
Syed Muhammad Ali Raza

Reputation: 374

  1. [Parameter] Val in Rating set by parent -> represent current rating; ValueChanged lets parent react to changes.

  2. @bind-Value=Model.Rating binds child rating to parent's Model.Rating, auto- updating; BlazorCreatedOnValueChanged updates parent when rating changes.

Upvotes: 0

Related Questions