Search code examples
c#blazorcomponentsblazor-webassemblywebassembly

Blazor InputDate not updating UI upon force change


I'm using the built in ASPNETCORE.Component.Forms InputDate but when selecting a date it does not override the UI when assigning another value it's always the selectedDate.

<InputDate ValueExpression="@(()=>MyModel.TestDate)"
                                   Value="MyModel.TestDate"
                                   ValueChanged="@((DateTime value) => CheckDate(value))"/>

private void CheckDate(DateTime selectedDate)
        {
            if (selectedDate < DateTime.Now)
            {
                MyModel.TestDate = selectedDate;
            }
            else
            {
                MyModel.TestDate = DateTime.Now; // Issue here UI does not updates
            }
            StateHasChanged();
        }

using WebAssembly 5.0.7


Solution

  • This is a difficult one to explain.

    Starting point: The current displayed value is today, 7-Sep-2023.

    1. When you change the data in the input to 8-Sep-2023 the new value gets passed back to InputDate though the OnChange event. At this point Value in InputDate is 7-Sep-2023: there's a data inconsistency. MyModel.TestDate is also set to 7-Sep-2023, and the value held in the Renderer's ParameterView is 7-Sep-2023.

    2. In your CheckDate code the new value is out of range, so you set MyModel.TestDate to 7-Sep-2023. You don't need to, it's still 7-Sep-2023: you haven't changed it.

    3. When the parent component renders, the value hasn't changed so 7-Sep-2023 is passed into the component as Value. InputDate will always render in the cascade as ValueExpression and ValueExpression are objects: as reference types the Renderer can't be sure if they have changed, so it renders anyway.

    4. InputDate still believes the value is 7-Sep-2023. Remember the data inconsistency in the first paragraph.

    5. Important bit The virtual DOM also believes it's 7-Sep-2023, so the diffing process doesn't see a change, and therefore doesn't refresh the raw input [which is actually at 8-Sep-2023]. It stays on the new value.

    The way to solve this dilemma is:

    1. Let the MyModel.TestDate get set to the new value
    2. Yield with await Task.Delay(1) which allows a render to reset the values to the new one in the render chain.
    3. Set the value to the validated value.
    4. Let the final UI event render [when the UI event completes] handle the update.

    Here's some demo code showing two ways to wire this up.

    @page "/"
    
    <PageTitle>Index</PageTitle>
    
    <h1>Hello, world!</h1>
    
    <InputDate class="mb-3" ValueExpression="@(()=>MyModel.TestDate)"
               Value="MyModel.TestDate"
               ValueChanged="@((DateTime value) => CheckDate(value))" />
    
    <InputDate class="form-control mb-3" @bind-Value="this.MyModel.TestDate1" @bind-Value:after="this.ValidateDate" />
    
    <div class="bg-dark text-white m-3 p-2">
        <pre>Date : @MyModel.TestDate.ToShortDateString()</pre>
        <pre>Date1 : @MyModel.TestDate1.ToShortDateString()</pre>
    </div>
    
    @code {
        private Model MyModel = new();
    
        private async Task ValidateDate()
        {
            await Task.Delay(1);
            if (MyModel.TestDate1 >= DateTime.Now)
                MyModel.TestDate1 = DateTime.Now; // Issue here UI does not updates
        }
    
        private async Task CheckDate(DateTime selectedDate)
        {
            MyModel.TestDate = selectedDate;
            await Task.Delay(1);
    
            if (selectedDate >= DateTime.Now)
                MyModel.TestDate = DateTime.Now; // Issue here UI does not updates
        }
    
        public class Model
        {
            public DateTime TestDate { get; set; }
            public DateTime TestDate1 { get; set; }
        }
    }