Reputation: 505
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:
StateHasChanged()
.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
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);
}
}
Upvotes: 1