65.9K
CodeProject 正在变化。 阅读更多。
Home

Blazor 编辑表单状态控件

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2021年3月16日

CPOL

5分钟阅读

viewsIcon

24386

一个 Blazor 控件,用于管理和监控表单中的编辑状态

概述 - Blazor 编辑表单状态控件

这是介绍一套有用的 Blazor 编辑控件的系列文章的第一篇,这些控件可以解决开箱即用的编辑体验中一些当前的不足,而无需购买昂贵的工具包。

EditForm

代码和示例

此存储库包含一个项目,其中实现了本系列所有文章的控件。您可以在此处找到它。

示例网站位于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 是整体包装器。它

  1. 创建 HTML Form 上下文。
  2. 连接任何 Submit 按钮 - 即,在表单中 type 设置为 submit 的按钮。
  3. 创建/管理 EditContext
  4. 级联 EditContextEditForm 内的所有控件都会以某种方式捕获并使用它。
  5. 为提交过程提供回调委托给父控件 - OnSubmitOnValidSubmitOnInvalidSubmit

EditContext

EditContext 是编辑过程的核心类,提供整体管理。它操作的数据类是 model:定义为 object 类型。它可以是任何对象,但实际上将是某种类型的数据类。唯一的前提是表单中使用的字段声明为 public 可读写属性。

EditContext 要么

  • 直接作为 EditContext 参数传递给 EditForm
  • 要么将模型对象实例设置为 Model 参数,然后 EditForm 从中创建一个 EditContext 实例。

要记住的一个重要点是,一旦创建了 EditContext 模型,就不要将其替换为另一个对象。虽然这可能是可能的,但不建议这样做。如果需要替换模型,请编写代码来刷新整个表单:安全第一!

FieldIdentifier

FieldIdentifier 类代表模型属性的部分“序列化”。EditContext 通过其 FieldIdentifier 来跟踪和标识单个属性。Model 是拥有该属性的对象,FieldName 是通过反射获得的属性名。

输入控件

InputTextInputNumber 和其他 InputBase 控件捕获级联的 EditContext。任何值更改都通过调用 NotifyFieldChanged 及其 FieldIdentifier 推送到 EditContext

EditContext 再探讨

EditContext 在内部维护一个 FieldIdentifier 列表。FieldIdentifier 对象在各种方法和事件中传递,以标识特定字段。调用 NotifyFieldChanged 会将 FieldIdentifier 对象添加到列表中。每当调用 NotifyFieldChanged 时,EditContext 都会触发 OnFieldChanged

IsModified 提供对列表或单个 FieldIdentifier 状态的访问。MarkAsUnmodified 重置单个 FieldIdentifier 或集合中的所有 FieldIdentifiers

EditContext 还包含管理验证的功能,但不是实际执行验证。我们将在下一篇文章中介绍验证过程。

EditFormState 控件

EditFormState 控件像所有编辑表单控件一样,捕获级联的 EditState。它所做的是

  1. 构建一个由 Model 公开的 public 属性列表,并维护每个属性的编辑状态 - 将原始值与编辑后的值进行相等性检查。
  2. 在字段值每次更改时更新状态。
  3. 通过一个 readonly 属性公开状态。
  4. 提供一个 EventCallback 委托,当编辑状态更新时触发。

在我们查看控件之前,让我们先看一下模型 - 在我们的例子中是 WeatherForecast - 以及一些支持类。

WeatherForecast

WeatherForecast 是一个典型的数据类。

  1. 每个字段都声明为带有默认值的属性。
  2. 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 是我们用于“序列化”模型属性的类。

  1. 基本字段是记录 - 它们只能在初始化时设置。
  2. EditedValue 存储字段的当前值。
  3. IsDirty 测试 ValueEditedValue 之间的相等性。
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

EditFieldCollectionEditFieldIEnumerable 集合。该类提供了一组受控的集合设置器和获取器,并实现了 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

属性是

  1. 从级联中拾取 EditContext
  2. 向父控件提供 EditStateChanged 回调,以告知它编辑状态已更改。
  3. 提供一个只读属性 IsDirty,供使用 @ref 的控件检查控件状态。
  4. EditFields 是我们填充并用于管理编辑状态的内部 EditFieldCollection
  5. disposedValueIDisposable 实现的一部分。
/// 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);
    }
}

简单的实现

为了测试该组件,这是一个简单的测试页面。

EditForm

上下改变温度,您应该会看到 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 日:初始版本
© . All rights reserved.