目录
概述——Blazor EditFormState控件
这是描述一组有用的Blazor Edit控件的系列文章中的第一篇,这些控件解决了开箱即用编辑体验中的一些当前缺点,而无需购买昂贵的工具包。
代码和示例
该存储库包含一个项目,该项目实现了本系列中所有文章的控件。你可以在这里找到它。
示例站点位于https://cec-blazor-database.azurewebsites.net/。
您可以在https://cec-blazor-database.azurewebsites.net//testeditor 中查看稍后描述的测试表单。
Repo是未来文章的一项正在进行的工作,因此会发生变化和发展。
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.。通过使用他们的FieldIdentifier调用NotifyFieldChanged,任何值的变动被向上推至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。暂时忽略这一点,我们将在下一篇文章中查看验证。我已经按照你在Repo代码中看到的方式展示了它。
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集合。该类为集合提供了一组受控的setter和getter,并为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中查找EditFiel并通过调用SetField设置它的EditedValue。然后使用当前的dirty状态触发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);
}
}
一个简单的实现
为了测试组件,这里有一个简单的测试页面。
上下更改温度,您应该会看到状态按钮更改颜色和文本。
您可以在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;
}
总结
如果您之前没有实现过此类功能,则此控件的真正优势可能不会立即显现出来,但我们将在后续文章中使用它来构建编辑器表单。在接下来的文章看起来在验证过程中,如何构建一个简单的自定义验证。第三篇文章着眼于表单锁定,使用此控件作为流程的一部分。
如果您在以后发现这篇文章很好,最新版本将在此处提供。
https://www.codeproject.com/Articles/5297299/A-Blazor-Edit-Form-State-Control