Blazor 编辑表单状态控件





5.00/5 (1投票)
一个 Blazor 控件,用于管理和监控表单中的编辑状态
概述 - Blazor 编辑表单状态控件
这是介绍一套有用的 Blazor 编辑控件的系列文章的第一篇,这些控件可以解决开箱即用的编辑体验中一些当前的不足,而无需购买昂贵的工具包。
代码和示例
此存储库包含一个项目,其中实现了本系列所有文章的控件。您可以在此处找到它。
示例网站位于https://cec-blazor-database.azurewebsites.net/。
您可以在 https://cec-blazor-database.azurewebsites.net//testeditor 处看到稍后描述的测试表单。
该存储库是为未来文章而进行的进行中的工作,将会发生变化和发展。
Blazor 编辑设置
首先,让我们看看当前的表单控件以及它们如何协同工作。一个经典的表单看起来像这样
<EditForm Model="@exampleModel" OnValidSubmit="@HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<InputText id="name" @bind-Value="exampleModel.Name" />
<ValidationMessage For="@(() => exampleModel.Name)" />
<button type="submit">Submit</button>
</EditForm>
EditForm
EditForm
是整体包装器。它
- 创建 HTML
Form
上下文。 - 连接任何
Submit
按钮 - 即,在表单中type
设置为submit
的按钮。 - 创建/管理
EditContext
。 - 级联
EditContext
。EditForm
内的所有控件都会以某种方式捕获并使用它。 - 为提交过程提供回调委托给父控件 -
OnSubmit
、OnValidSubmit
和OnInvalidSubmit
。
EditContext
EditContext
是编辑过程的核心类,提供整体管理。它操作的数据类是 model
:定义为 object
类型。它可以是任何对象,但实际上将是某种类型的数据类。唯一的前提是表单中使用的字段声明为 public
可读写属性。
EditContext
要么
- 直接作为
EditContext
参数传递给EditForm
, - 要么将模型对象实例设置为
Model
参数,然后EditForm
从中创建一个EditContext
实例。
要记住的一个重要点是,一旦创建了 EditContext
模型,就不要将其替换为另一个对象。虽然这可能是可能的,但不建议这样做。如果需要替换模型,请编写代码来刷新整个表单:安全第一!
FieldIdentifier
FieldIdentifier
类代表模型属性的部分“序列化”。EditContext
通过其 FieldIdentifier
来跟踪和标识单个属性。Model
是拥有该属性的对象,FieldName
是通过反射获得的属性名。
输入控件
InputText
、InputNumber
和其他 InputBase
控件捕获级联的 EditContext
。任何值更改都通过调用 NotifyFieldChanged
及其 FieldIdentifier
推送到 EditContext
。
EditContext 再探讨
EditContext
在内部维护一个 FieldIdentifier
列表。FieldIdentifier
对象在各种方法和事件中传递,以标识特定字段。调用 NotifyFieldChanged
会将 FieldIdentifier
对象添加到列表中。每当调用 NotifyFieldChanged
时,EditContext
都会触发 OnFieldChanged
。
IsModified
提供对列表或单个 FieldIdentifier
状态的访问。MarkAsUnmodified
重置单个 FieldIdentifier
或集合中的所有 FieldIdentifiers
。
EditContext
还包含管理验证的功能,但不是实际执行验证。我们将在下一篇文章中介绍验证过程。
EditFormState 控件
EditFormState
控件像所有编辑表单控件一样,捕获级联的 EditState
。它所做的是
- 构建一个由
Model
公开的public
属性列表,并维护每个属性的编辑状态 - 将原始值与编辑后的值进行相等性检查。 - 在字段值每次更改时更新状态。
- 通过一个
readonly
属性公开状态。 - 提供一个
EventCallback
委托,当编辑状态更新时触发。
在我们查看控件之前,让我们先看一下模型 - 在我们的例子中是 WeatherForecast
- 以及一些支持类。
WeatherForecast
WeatherForecast
是一个典型的数据类。
- 每个字段都声明为带有默认值的属性。
Validate
实现IValidation
。暂时忽略这一点,我们将在下一篇文章中讨论验证。我之所以展示它,是因为您会在存储库代码中看到它。
public class WeatherForecast : IValidation
{
public int ID { get; set; } = -1;
public DateTime Date { get; set; } = DateTime.Now;
public int TemperatureC { get; set; } = 0;
[NotMapped] public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public string Summary { get; set; } = string.Empty;
/// Ignore for now, but as you'll see it in the example repo it's shown
public bool Validate(ValidationMessageStore validationMessageStore, string fieldname, object model = null)
{
....
}
}
EditField
EditField
是我们用于“序列化”模型属性的类。
- 基本字段是记录 - 它们只能在初始化时设置。
EditedValue
存储字段的当前值。IsDirty
测试Value
和EditedValue
之间的相等性。
public class EditField
{
public string FieldName { get; init; }
public Guid GUID { get; init; }
public object Value { get; init; }
public object Model { get; init; }
public object EditedValue { get; set; }
public bool IsDirty
{
get
{
if (Value != null && EditedValue != null) return !Value.Equals(EditedValue);
if (Value is null && EditedValue is null) return false;
return true;
}
}
public EditField(object model, string fieldName, object value)
{
this.Model = model;
this.FieldName = fieldName;
this.Value = value;
this.EditedValue = value;
this.GUID = Guid.NewGuid();
}
public void Reset()
=> this.EditedValue = this.Value;
}
EditFieldCollection
EditFieldCollection
是 EditField
的 IEnumerable
集合。该类提供了一组受控的集合设置器和获取器,并实现了 IEnumerable
接口的必要方法。它还提供了一个 IsDirty
属性来公开集合的状态。
public class EditFieldCollection : IEnumerable
{
private List<EditField> _items = new List<EditField>();
public int Count => _items.Count;
public Action<bool> FieldValueChanged;
public bool IsDirty => _items.Any(item => item.IsDirty);
public void Clear()
=> _items.Clear();
public void ResetValues()
=> _items.ForEach(item => item.Reset());
public IEnumerator GetEnumerator()
=> new EditFieldCollectionEnumerator(_items);
public T Get<T>(string FieldName)
{
var x = _items.FirstOrDefault(item => item.FieldName.Equals
(FieldName, StringComparison.CurrentCultureIgnoreCase));
if (x != null && x.Value is T t) return t;
return default;
}
public T GetEditValue<T>(string FieldName)
{
var x = _items.FirstOrDefault(item => item.FieldName.Equals
(FieldName, StringComparison.CurrentCultureIgnoreCase));
if (x != null && x.EditedValue is T t) return t;
return default;
}
public bool TryGet<T>(string FieldName, out T value)
{
value = default;
var x = _items.FirstOrDefault(item => item.FieldName.Equals
(FieldName, StringComparison.CurrentCultureIgnoreCase));
if (x != null && x.Value is T t) value = t;
return x.Value != default;
}
public bool TryGetEditValue<T>(string FieldName, out T value)
{
value = default;
var x = _items.FirstOrDefault(item => item.FieldName.Equals
(FieldName, StringComparison.CurrentCultureIgnoreCase));
if (x != null && x.EditedValue is T t) value = t;
return x.EditedValue != default;
}
public bool HasField(EditField field)
=> this.HasField(field.FieldName);
public bool HasField(string FieldName)
{
var x = _items.FirstOrDefault(item => item.FieldName.Equals
(FieldName, StringComparison.CurrentCultureIgnoreCase));
if (x is null | x == default) return false;
return true;
}
public bool SetField(string FieldName, object value)
{
var x = _items.FirstOrDefault(item => item.FieldName.Equals
(FieldName, StringComparison.CurrentCultureIgnoreCase));
if (x != null && x != default)
{
x.EditedValue = value;
this.FieldValueChanged?.Invoke(this.IsDirty);
return true;
}
return false;
}
public bool AddField(object model, string fieldName, object value)
{
this._items.Add(new EditField(model, fieldName, value));
return true;
}
Enumerator
支持类。
public class EditFieldCollectionEnumerator : IEnumerator
{
private List<EditField> _items = new List<EditField>();
private int _cursor;
object IEnumerator.Current
{
get
{
if ((_cursor < 0) || (_cursor == _items.Count))
throw new InvalidOperationException();
return _items[_cursor];
}
}
public EditFieldCollectionEnumerator(List<EditField> items)
{
this._items = items;
_cursor = -1;
}
void IEnumerator.Reset()
=> _cursor = -1;
bool IEnumerator.MoveNext()
{
if (_cursor < _items.Count)
_cursor++;
return (!(_cursor == _items.Count));
}
}
}
现在我们已经看到了支持类,接下来是主控件。
EditFormState
EditFormState
被声明为一个组件并实现 IDisposable
。
public class EditFormState : ComponentBase, IDisposable
属性是
- 从级联中拾取
EditContext
。 - 向父控件提供
EditStateChanged
回调,以告知它编辑状态已更改。 - 提供一个只读属性
IsDirty
,供使用@ref
的控件检查控件状态。 EditFields
是我们填充并用于管理编辑状态的内部EditFieldCollection
。disposedValue
是IDisposable
实现的一部分。
/// EditContext - cascaded from EditForm
[CascadingParameter] public EditContext EditContext { get; set; }
/// EventCallback for parent to link into for Edit State Change Events
/// passes the current Dirty state
[Parameter] public EventCallback<bool> EditStateChanged { get; set; }
/// Property to expose the Edit/Dirty state of the control
public bool IsDirty => EditFields?.IsDirty ?? false;
private EditFieldCollection EditFields = new EditFieldCollection();
private bool disposedValue;
当组件初始化时,它会捕获 Model
属性并将 EditFields
填充为初始数据。最后一步是将 EditContext.OnFieldChanged
连接到 FieldChanged
,以便在字段值更改时调用 FieldChanged
。
protected override Task OnInitializedAsync()
{
Debug.Assert(this.EditContext != null);
if (this.EditContext != null)
{
// Populates the EditField Collection
this.GetEditFields();
// Wires up to the EditContext OnFieldChanged event
this.EditContext.OnFieldChanged += FieldChanged;
}
return Task.CompletedTask;
}
/// Method to populate the edit field collection
protected void GetEditFields()
{
// Gets the model from the EditContext and populates the EditFieldCollection
this.EditFields.Clear();
var model = this.EditContext.Model;
var props = model.GetType().GetProperties();
foreach (var prop in props)
{
var value = prop.GetValue(model);
EditFields.AddField(model, prop.Name, value);
}
}
FieldChanged
事件处理程序从 EditFields
中查找 EditField
,并通过调用 SetField
来设置其 EditedValue
。然后,它触发 EditStateChanged
回调,并带有当前的脏状态。
/// Event Handler for Editcontext.OnFieldChanged
private void FieldChanged(object sender, FieldChangedEventArgs e)
{
// Get the PropertyInfo object for the model property
// Uses reflection to get property and value
var prop = e.FieldIdentifier.Model.GetType().GetProperty(e.FieldIdentifier.FieldName);
if (prop != null)
{
// Get the value for the property
var value = prop.GetValue(e.FieldIdentifier.Model);
// Sets the edit value in the EditField
EditFields.SetField(e.FieldIdentifier.FieldName, value);
// Invokes EditStateChanged
this.EditStateChanged.InvokeAsync(EditFields?.IsDirty ?? false);
}
}
最后,我们有一些实用方法和 IDisposable
实现。
/// Method to Update the Edit State to current values
public void UpdateState()
{
this.GetEditFields();
this.EditStateChanged.InvokeAsync(EditFields?.IsDirty ?? false);
}
// IDisposable Implementation
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
if (this.EditContext != null)
this.EditContext.OnFieldChanged -= this.FieldChanged;
}
disposedValue = true;
}
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
简单的实现
为了测试该组件,这是一个简单的测试页面。
上下改变温度,您应该会看到 State 按钮改变颜色和文本。
您可以在 https://cec-blazor-database.azurewebsites.net/editstateeditor 处看到这个示例的实际运行效果。
@using Blazor.Database.Data
@page "/test"
<EditForm Model="@Model" OnValidSubmit="@HandleValidSubmit">
<EditFormState @ref="editFormState" EditStateChanged="this.EditStateChanged">
</EditFormState>
<label class="form-label">ID:</label> <InputNumber class="form-control"
@bind-Value="Model.ID" />
<label class="form-label">Date:</label> <InputDate class="form-control"
@bind-Value="Model.Date" />
<label class="form-label">Temp C:</label> <InputNumber class="form-control"
@bind-Value="Model.TemperatureC" />
<label class="form-label">Summary:</label> <InputText class="form-control"
@bind-Value="Model.Summary" />
<div class="text-right mt-2">
<button class="btn @btncolour">@btntext</button>
<button class="btn btn-primary" type="submit">Submit</button>
</div>
<div>
</div>
</EditForm>
@code {
protected bool _isDirty = false;
protected string btncolour => _isDirty ? "btn-danger" : "btn-success";
protected string btntext => _isDirty ? "Dirty" : "Clean";
protected EditFormState editFormState { get; set; }
private WeatherForecast Model = new WeatherForecast()
{
ID = 1,
Date = DateTime.Now,
TemperatureC = 22,
Summary = <span class="pl-pds">"Balmy"
};
private void HandleValidSubmit()
{
this.editFormState.UpdateState();
}
private void EditStateChanged(bool editstate)
=> this._isDirty = editstate;
}
总结
虽然这个控件的真正好处可能对于之前没有实现过此类功能的人来说并不明显,但我们将在后续文章中使用它来构建一个编辑器表单。下一篇文章介绍验证过程以及如何构建一个简单的自定义验证器。第三篇文章介绍表单锁定,并将此控件作为该过程的一部分。
如果您在很久以后发现这篇文章,最新版本将在 这里 提供。
历史
- 2021 年 3 月 16 日:初始版本