我遇到了一个问题,服务器端Blazor试图创建一个自定义表组件。表行中的数据更新和动态变化,所以这不是问题,但如果我绑定属性的头,头将采用该属性的前一个值。
从表。剃刀,我正在设置一个简单的下拉<select>
标签与默认值。当这个值被改变时,它应该更新表头上的值。
我添加了一个<code>
标签和一个经典的HTML表作为测试,它们都正确地反映了新的<select>
值。知道为什么自定义组件不一样吗?
Table.razor
@page "/table"
@using BlazorApp1.Data
@inject WeatherForecastService ForecastService
<select @bind="@SelectedListItem">
<option value="test">Test</option>
<option value="test2">Test2</option>
</select>
<code>@SelectedListItem</code>
<table>
<thead>
<tr>
<th>@SelectedListItem</th>
</tr>
<tbody>
<tr>
<td>1</td>
</tr>
<tr>
<td>2</td>
</tr>
<tr>
<td>3</td>
</tr>
<tr>
<td>4</td>
</tr>
<tr>
<td>5</td>
</tr>
</tbody>
</thead>
</table>
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<BlazorApp1.Components.DataTable Items=@forecasts TRowItem="WeatherForecast">
<BlazorApp1.Components.Column CustomTitle="@SelectedListItem" TRowItem="WeatherForecast"></BlazorApp1.Components.Column>
</BlazorApp1.Components.DataTable>
}
@code{
public string SelectedListItem { get; set; }
private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync()
{
forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
}
}
DataTable.cs
using Microsoft.AspNetCore.Components;
namespace BlazorApp1.Components
{
public partial class DataTable<TRowItem> : ComponentBase
{
[Parameter]
public IList<TRowItem> Items { get; set; } = new List<TRowItem>();
[Parameter]
public RenderFragment? ChildContent { get; set; }
private IList<Column<TRowItem>> Columns { get; set; } = new List<Column<TRowItem>>();
protected override void OnInitialized()
{
if (Items == null) Items = new List<TRowItem>();
}
protected override async Task OnParametersSetAsync()
{
await UpdateAsync().ConfigureAwait(false);
}
public async Task UpdateAsync()
{
Refresh();
}
public void Refresh()
{
InvokeAsync(StateHasChanged);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
foreach (var column in Columns)
{
column.StateChanged += ColumnStateChanged;
}
StateHasChanged();
}
}
public void Dispose()
{
foreach (var column in Columns)
{
column.StateChanged -= ColumnStateChanged;
}
Items.Clear();
}
public void AddColumn(Column<TRowItem> column)
{
Columns.Add(column);
StateHasChanged();
}
private void ColumnStateChanged(Object? sender, EventArgs args) => StateHasChanged();
}
}
DataTable.razor
@typeparam TRowItem
<h3>DataTable</h3>
<CascadingValue Value="this">
<div>
<table>
<thead>
<tr>
@foreach(var column in Columns)
{
<th nowrap>@column.CustomTitle</th>
}
</tr>
</thead>
<tbody>
@foreach(var item in Items)
{
foreach(var column in Columns)
{
<td></td>
}
}
</tbody>
<tfoot>
</tfoot>
</table>
</div>
@ChildContent
</CascadingValue>
Column.cs
using Microsoft.AspNetCore.Components;
using System.Linq.Expressions;
namespace BlazorApp1.Components
{
public partial class Column<TRowItem> : ComponentBase
{
[CascadingParameter]
private DataTable<TRowItem>? DataTable { get; set; }
[Parameter]
public string? CustomTitle { get; set; }
[Parameter]
public Expression<Func<TRowItem, object>>? Property { get; set; }
protected override Task OnInitializedAsync()
{
if (DataTable == null) throw new ArgumentNullException($"A 'DataTableColumn' must be a child of a 'DataTable' component");
DataTable.AddColumn(this);
return Task.CompletedTask;
}
public event EventHandler? StateChanged;
private void RaiseStateChanged()
{
EventHandler? handler = StateChanged;
handler?.Invoke(this, new EventArgs());
}
}
}
Column.razor
@typeparam TRowItem
知道为什么自定义组件不一样吗?
选择中的任何更改都会导致页面中的Blazor UI事件,从而触发重新渲染事件。渲染器通过触发组件上的SetParametersAsync
来完成此操作。组件更新其参数,运行OnParametersSet{Async}
并重新渲染。
DataTable
包含Column
,所以它先重新渲染。此时Column
中的CustomTitle
还没有运行SetParametersAsync
,所以DataTable
渲染当前的"旧的";价值。Column
然后重新渲染设置CustomTitle
为新值。
根据您所展示的代码,答案是在Column
组件中呈现标签。但我猜这在现实中要复杂一些,所以这可能不是答案。您可能希望它在标题部分中显示标题,并在行模板中显示值。如果是这样的话,我可以给你一些代码来展示如何做到这一点。
解决方案不是做更多的管道或放入更多的StateHasChanged
,而是重新思考你在做什么。获取数据,即表配置到一个/多个数据类中,并将其级联到您的各个组件。或者使用DI服务来保存数据,并通过事件驱动组件更新。