Chris Thompson
Chris Thompson

Reputation: 505

Blazor MAUI extracted component not updating list

I'm trying out .NET MAUI with Blazor, and running into issues when I extract some code from one component into a new component.

Given a list of a class which looks like this:

public class Expense
{
    public DateTime Date { get; set; }

    public decimal Amount { get; set; }

    public string Note { get; set; }

    public Expense(DateTime date, decimal amount, string note)
    {
        Date = date;
        Amount = amount;
        Note = note;
    }
}

When I work such a list inside of a Blazor component, running inside of .NET MAUI, everything works fine when I have it all inside of one component, such as:

<button class="btn btn-primary bg-color-green" @onclick="AddExpense"><i class="oi oi-plus"></i></button>

<table class="table">
    <thead>
        <tr>
            <th>Date</th>
            <th>Amount</th>
            <th>Note</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @for (var i = 0; i < expenses.Count; i++)
        {
            int copy = i;
            <tr>
                <td>
                    <input type="date" min="2022-01-01" max="@currentDay"
                       @bind="@expenses[copy].Date"
                       @bind:format="yyyy-MM-dd" />
                </td>
                <td>
                    <input type="number" min="0" step="0.01" @bind="@expenses[copy].Amount" />
                </td>
                <td>
                    <input type="text" @bind="@(expenses[copy].Note)" />
                </td>
                <td>
                    <button class="btn" @onclick="() => expenses.RemoveAt(copy)"><i class="oi oi-delete"></i></button>
                </td>
            </tr>
        }
    </tbody>
</table>

@code { region

    private readonly string currentDay = $"{DateTime.Now.Year}-{DateTime.Now.Month}-{DateTime.Now.Day}";

    private List<Expense> expenses = new List<Expense>()
    {
        new Expense(DateTime.Today, 100, "test")
    };

    private void AddExpense()
    {
        expenses.Add(new Expense(DateTime.Today, 0, ""));
    }

However, I attempted to extract the table rows into their own ExpenseRow component, which looks like this:

<tr>
    <td>
        <input type="date" min="@MinAttributeValue" max="@MaxAttributeValue"
               @bind="@Date"
               @bind:format="yyyy-MM-dd"/>
    </td>
    <td>
        <input type="number" min="0" step="0.01" @bind="@Amount"/>
    </td>
    <td>
        <input type="text" @bind="@Note"/>
    </td>
    <td>
        <button class="btn" @onclick="() => OnRemoveClick()"><i class="oi oi-delete"></i></button>
    </td>
</tr>

@code { region:

    private string MinAttributeValue => Min.ToString("yyyy-MM-dd");
    private string MaxAttributeValue => Max?.ToString("yyyy-MM-dd");

    [Parameter]
    public DateTime Min { get; set; } = new DateTime(2022, 1, 1);

    [Parameter]
    public DateTime? Max { get; set; }

    [Parameter]
    public DateTime Date { get; set; }

    [Parameter]
    public EventCallback<DateTime> DateChanged { get; set; }

    [Parameter]
    public decimal Amount { get; set; }

    [Parameter]
    public EventCallback<decimal> AmountChanged { get; set; }

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

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

    [Parameter]
    public Action OnRemoveClick { get; set; }

And I updated the original component to render this new component in the for loop:

<table class="table">
    <thead>
        <tr>
            <th>Date</th>
            <th>Amount</th>
            <th>Note</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @for (var i = 0; i < expenses.Count; i++)
        {
            int copy = i;
            <ExpenseRow 
                @bind-Amount="expenses[copy].Amount"
                @bind-Date="expenses[copy].Date"
                @bind-Note="expenses[copy].Note"
                Max="@DateTime.Today"
                OnRemoveClick="() => { expenses.RemoveAt(copy); StateHasChanged(); }" />
        }
    </tbody>
</table>

This change apparently breaks the binding back to the objects in the list:

I also attempted to invoke StateHasChanged() in this way, based on this answer:

int copy = i;
            <ExpenseRow 
                @bind-Amount="expenses[copy].Amount"
                AmountChanged="() => StateHasChanged()"
                @bind-Date="expenses[copy].Date"
                DateChanged="() => StateHasChanged()"
                @bind-Note="expenses[copy].Note"
                NoteChanged="() => StateHasChanged()"
                Max="@DateTime.Today"
                OnRemoveClick="() => { expenses.RemoveAt(copy); StateHasChanged(); }" />

However, this does not resolve the issue with changes being undone as soon as I add a value to the list.

Upvotes: 1

Views: 764

Answers (1)

Zhi Lv
Zhi Lv

Reputation: 21581

In the ExpenseRow component, you can add the oninput event on the input elements, in this event trigger a method to update the value and invokes the related EventCallback event. Then, the parent component has also been updated to use bind-Value when passing its parameter to the child component.

Code like this:

Parent component (ExpenseIndex.razor):

@page "/expenseIndex"
@using MauiApp1.Data
<h3>ExpenseIndex</h3>

<button class="btn btn-primary bg-color-green" @onclick="AddExpense"><i class="oi oi-plus"></i></button>

<table class="table">
    <thead>
        <tr>
            <th>Date</th>
            <th>Amount</th>
            <th>Note</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @for (var i = 0; i < expenses.Count; i++)
        {
            int copy = i;
            <ExpenseRow @bind-Amount="expenses[copy].Amount"
                    @bind-Date="expenses[copy].Date"
                    @bind-Note="expenses[copy].Note"
                    Max="@DateTime.Today" 
                    OnRemoveClick="() => { expenses.RemoveAt(copy); StateHasChanged(); }" />
        }
    </tbody>
</table> 
@code {
    private readonly string currentDay = $"{DateTime.Now.Year}-{DateTime.Now.Month}-{DateTime.Now.Day}";

    private List<Expense> expenses = new List<Expense>()
    {
        new Expense(DateTime.Today, 100, "test")
    };

    private void AddExpense()
    {
        expenses.Add(new Expense(DateTime.Today, 0, ""));
    }
}

Child component: ExpenseRow.razor page:

<tr>
    <td>
        <input type="date" min="@MinAttributeValue" max="@MaxAttributeValue"
               @bind="@Date" 
               @bind:format="yyyy-MM-dd" @oninput="@OnDateInputChange" />
    </td>
    <td>
        <input type="number" min="0" step="0.01" @bind="@Amount" @oninput="@OnAmountInputChange" />
    </td>
    <td>
        <input type="text" @bind="@Note" @oninput="@OnNoteInputChange" />
    </td>
    <td>
        <button class="btn" @onclick="() => OnRemoveClick()"><i class="oi oi-delete"></i></button>
    </td>
</tr>
@code {
    private string MinAttributeValue => Min.ToString("yyyy-MM-dd");
    private string MaxAttributeValue => Max?.ToString("yyyy-MM-dd");

    [Parameter]
    public DateTime Min { get; set; } = new DateTime(2022, 1, 1);

    [Parameter]
    public DateTime? Max { get; set; }

    [Parameter]
    public DateTime Date { get; set; }

    [Parameter]
    public EventCallback<DateTime> DateChanged { get; set; }

    [Parameter]
    public decimal Amount { get; set; }

    [Parameter]
    public EventCallback<decimal> AmountChanged { get; set; }

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

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

    [Parameter]
    public Action OnRemoveClick { get; set; }

    private async Task OnDateInputChange(ChangeEventArgs args)
    {
        Date = Convert.ToDateTime( args.Value);
        await DateChanged.InvokeAsync(Date);
    }
    private async Task OnAmountInputChange(ChangeEventArgs args)
    {
        Amount = Convert.ToDecimal(args.Value);
        await AmountChanged.InvokeAsync(Amount);
    }
    private async Task OnNoteInputChange(ChangeEventArgs args)
    {
        Note = args.Value.ToString();
        await NoteChanged.InvokeAsync(Note);
    }
}

Then, the output as below: enter image description here

Upvotes: 1

Related Questions