Blazor 编辑状态跟踪器





5.00/5 (1投票)
本文介绍了如何为 Blazor 构建一个编辑状态追踪器,该追踪器集成到 EditForm 和 EditContext 中。
下面的截图显示了一个脏的、无效的表单,我点击了浏览器刷新按钮,试图退出这个脏的表单。
注意:此实现仅跟踪扁平的单层对象。 如果您想跟踪嵌套对象,您需要构建自己的编辑上下文。
代码仓库
您可以在 Blazor Server 应用程序的 Blazr.EditStateTracker 中找到代码。
EditContext 和 InputBase 组件如何交互
EditContext
维护一个内部字典,其中包含定义为 FieldIdentifier
/FieldState
对的 编辑状态。
FieldIdentifier
定义为
public readonly struct FieldIdentifier : IEquatable<FieldIdentifier>
{
public object Model { get; }
public string FieldName { get; }
//....
}
而 FieldState
定义为
internal sealed class FieldState
{
public bool IsModified {get; set;}
//...
}
所有 InputBase
控件在更新时都会调用 EditContext.NotifyFieldChanged
。NotifyFieldChanged
在编辑状态字典中添加或更新一个条目,并引发 OnFieldChanged
事件。
public event EventHandler<FieldChangedEventArgs>? OnFieldChanged;
public void NotifyFieldChanged(in FieldIdentifier fieldIdentifier)
{
GetOrAddFieldState(fieldIdentifier).IsModified = true;
OnFieldChanged?.Invoke(this, new FieldChangedEventArgs(fieldIdentifier));
}
internal FieldState GetOrAddFieldState(in FieldIdentifier fieldIdentifier)
{
if (!_fieldStates.TryGetValue(fieldIdentifier, out var state))
{
state = new FieldState(fieldIdentifier);
_fieldStates.Add(fieldIdentifier, state);
}
return state;
}
InputBase
组件通过一些相当复杂的 Css 提供程序代码使用字段状态来获取组件的 css 格式。 对于已修改且有效的,显示为 *绿色*,对于无效的,显示为 *红色*。 下面显示了各个类的代码片段供参考。
获取 InputBase
的 Css 的代码。
protected string CssClass
{
get
{
var fieldClass = EditContext?.FieldCssClass(FieldIdentifier);
return AttributeUtilities.CombineClassNames
(AdditionalAttributes, fieldClass) ?? string.Empty;
}
}
在 EditContextFieldClassExtensions
中定义的 FieldCssClass
扩展方法
public static string FieldCssClass
(this EditContext editContext, in FieldIdentifier fieldIdentifier)
{
var provider = editContext.Properties.TryGetValue
(FieldCssClassProviderKey, out var customProvider)
? (FieldCssClassProvider)customProvider
: FieldCssClassProvider.Instance;
return provider.GetFieldCssClass(editContext, fieldIdentifier);
}
以及默认的 FieldCssClassProvider
提供程序。
public class FieldCssClassProvider
{
internal static readonly FieldCssClassProvider Instance =
new FieldCssClassProvider();
public virtual string GetFieldCssClass
(EditContext editContext, in FieldIdentifier fieldIdentifier)
{
var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();
if (editContext.IsModified(fieldIdentifier))
{
return isValid ? "modified valid" : "modified invalid";
}
else
{
return isValid ? "valid" : "invalid";
}
}
}
实现
该实现由四个对象组成
TrackStateAttribute
- 一个自定义属性,用于标识要跟踪的属性EditStateProperty
- 一个用于保存属性状态数据的类EditStateStore
- 一个用于保存被跟踪的EditContext.Model
真实状态的集合类EditStateTracker
- 一个嵌入在EditForm
中并将所有内容连接起来并对EditContext
中的不一致进行排序的组件
TrackState
用于标识已跟踪属性的自定义属性。 它只标识要跟踪的属性。
public class TrackStateAttribute : Attribute {}
应用于 WeatherForecast
public class WeatherForecast
{
[TrackState] public DateOnly Date { get; set; }
[TrackState] public int TemperatureC { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
[TrackState] [Required] public string? Summary { get; set; }
}
EditStateProperty
EditStateProperty
跟踪各个属性的状态。
public class EditStateProperty
{
public string Name { get; private set; }
public object? BaseValue { get; private set; }
public object? CurrentValue { get; private set; }
public EditStateProperty(string name, object? value)
{
Name = name;
BaseValue = value;
CurrentValue= value;
}
public void Set(object? value)
=> CurrentValue = value;
public bool IsDirty => !BaseValue?.Equals(CurrentValue) ?? CurrentValue is not null;
}
EditStateStore
EditStateStore
是维护属性状态列表的集合对象。 该类在 ctor
中需要 EditContext
并跟踪 EditContext.Model
。 它通过反射获取可跟踪属性,并构建一个 EditStateProperty
对象列表。
Update
更新属性值并管理 EditContext
上的真实字段状态。
IsDirty
提供对象或单个属性状态。
public class EditStateStore
{
private object _model = new();
private List<EditStateProperty> _properties = new();
private EditContext _editContext;
public EditStateStore(EditContext context)
{
_editContext = context;
_model = context.Model;
var props = _model.GetType().GetProperties().Where(
prop => Attribute.IsDefined(prop, typeof(TrackStateAttribute)));
foreach (var prop in props)
{
_properties.Add(new(prop.Name, prop.GetValue(_model)));
}
}
public void Update(FieldChangedEventArgs e)
{
var property = _properties.FirstOrDefault
(item => item.Name.Equals(e.FieldIdentifier.FieldName));
if (property != null)
{
var propInfo = e.FieldIdentifier.Model.GetType().GetProperty
(e.FieldIdentifier.FieldName);
if (propInfo != null)
{
var value = propInfo.GetValue(e.FieldIdentifier.Model);
property.Set(value);
// If the value is clean clear out the modified setting
// in the Edit Context
if (!IsDirty(e.FieldIdentifier.FieldName))
_editContext.MarkAsUnmodified(e.FieldIdentifier);
}
}
}
public bool IsDirty(string fieldName)
=> _properties.FirstOrDefault
(item => item.Name.Equals(fieldName))?.IsDirty ?? false;
public bool IsDirty()
=> _properties.Any(item => item.IsDirty);
}
EditStateTracker
EditStateTracker
是一个在 EditForm
中将所有内容连接在一起的组件。
组件
- 捕获
EditContext
。 - 创建一个
EditStateStore
。 - 将处理程序连接到
EditContext
的OnFieldChanged
事件。
OnFieldChanged
在存储上调用 Update
,如果编辑状态发生更改,则调用 EditStateChanged
。
LockNavigation
启用/禁用导航锁定。如果需要,UI 添加 NavigationLock
组件并将其连接起来。
OnLocationChanged
是 NavigationLock
的回调处理程序,用于在表单脏时阻止导航。
@implements IDisposable
@if(this.LockNavigation)
{
<NavigationLock OnBeforeInternalNavigation=
this.OnLocationChanged ConfirmExternalNavigation=_isDirty />
}
@code {
[CascadingParameter] private EditContext _editContext { get; set; } = default!;
[Parameter] public bool LockNavigation { get; set; }
[Parameter] public EventCallback<bool> EditStateChanged { get; set; }
private EditStateStore _store = default!;
private bool _currentIsDirty = false;
private bool _isDirty => _store.IsDirty();
public EditStateTracker() { }
protected override void OnInitialized()
{
ArgumentNullException.ThrowIfNull(_editContext);
_store = new(_editContext);
ArgumentNullException.ThrowIfNull(_store);
_editContext.OnFieldChanged += OnFieldChanged;
}
private void OnFieldChanged(object? sender, FieldChangedEventArgs e)
{
_store.Update(e);
if (_isDirty != _currentIsDirty)
{
_currentIsDirty = _isDirty;
this.EditStateChanged.InvokeAsync(_isDirty);
}
}
private void OnLocationChanged(LocationChangingContext context)
{
if (_isDirty)
context.PreventNavigation();
}
public void Dispose()
=> _editContext.OnFieldChanged -= OnFieldChanged;
}
编辑表单
这是一个非常标准的编辑表单。 请注意
- 添加到
EditForm
的EditStateTracker
组件。 - 通过
EditStateTracker
上的EditStateChanged
跟踪编辑状态,并使用它来更改按钮的状态。 - 包含验证以显示其有效性。
- 有一个模拟保存以演示如何实现它。
@page "/"
<PageTitle>Index</PageTitle>
<EditForm EditContext=_editContext>
<DataAnnotationsValidator />
<EditStateTracker @ref=_editStateTracker
EditStateChanged=this.OnEditStateChanged LockNavigation=true />
<div class="mb-3">
<label class="form-label">Date</label>
<InputDate class="form-control" @bind-Value=this.model.Date />
</div>
<div class="mb-3">
<label class="form-label">Temperature °C</label>
<InputNumber class="form-control" @bind-Value=this.model.TemperatureC />
</div>
<div class="mb-3">
<label class="form-label">Summary</label>
<InputSelect class="form-select" @bind-Value=this.model.Summary>
@if (this.model.Summary is null)
{
<option disabled selected value=""> -- Choose a Summary --</option>
}
@foreach (var summary in Summaries)
{
<option value="@summary">@summary</option>
}
</InputSelect>
<ValidationMessage For="() => this.model.Summary" />
</div>
<div class="mb-3 text-end">
<button disabled="@(!_isDirty)" type="button"
class="btn btn-success" @onclick=this.SaveAsync>Submit</button>
<button disabled="@(_isDirty)" type="button"
class="btn btn-dark">Exit</button>
</div>
</EditForm>
<div class="bg-dark text-white m-4 p-2">
<pre>Date : @this.model.Date</pre>
<pre>Temperature °C : @this.model.TemperatureC</pre>
<pre>Summary: @this.model.Summary</pre>
<pre>State: @(_isDirty ? "Dirty" : "Clean")</pre>
</div>
@code {
private EditStateTracker? _editStateTracker;
private bool _isDirty;
private WeatherForecast model = new()
{ Date = DateOnly.FromDateTime(DateTime.Now), TemperatureC = 10 };
private EditContext? _editContext;
protected override void OnInitialized()
=> _editContext = new EditContext(model);
private void OnEditStateChanged(bool isDirty)
=> _isDirty = isDirty;
private async Task SaveAsync()
{
if (_editContext?.Validate() ?? false)
{
// mock an async call to the data pipeline to save the record
await Task.Delay(100);
// Error handling code here
// This will reset the edit context and the EditStateTracker
_editContext = new EditContext(model);
_isDirty = false;
}
}
private List<string> Summaries = new()
{ "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm",
"Balmy", "Hot", "Sweltering", "Scorching" };
}
刷新/重置编辑上下文和状态
没有刷新或重置状态的机制,因为 EditContext
没有任何重置自身的机制。
在表单中,SaveAsync
基于保存的模型创建一个新的 EditContext
。EditForm
检测到新的 EditContext
,并强制渲染器销毁旧的组件并重建其内容。
这是 EditForm
的相关代码。
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
Debug.Assert(_editContext != null);
// If _editContext changes, tear down and recreate all descendants.
// This is so we can safely use the IsFixed optimization on CascadingValue,
// optimizing for the common case where _editContext never changes.
builder.OpenRegion(_editContext.GetHashCode());
builder.OpenElement(0, "form");
builder.AddMultipleAttributes(1, AdditionalAttributes);
builder.AddAttribute(2, "onsubmit", _handleSubmitDelegate);
builder.OpenComponent<CascadingValue<EditContext>>(3);
builder.AddComponentParameter(4, "IsFixed", true);
builder.AddComponentParameter(5, "Value", _editContext);
builder.AddComponentParameter
(6, "ChildContent", ChildContent?.Invoke(_editContext));
builder.CloseComponent();
builder.CloseElement();
builder.CloseRegion();
}
历史
- 2023年3月30日:初始版本