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

Blazor 编辑状态跟踪器

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2023年3月30日

CPOL

3分钟阅读

viewsIcon

10109

本文介绍了如何为 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.NotifyFieldChangedNotifyFieldChanged 在编辑状态字典中添加或更新一个条目,并引发 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";
        }
    }
}

实现

该实现由四个对象组成

  1. TrackStateAttribute - 一个自定义属性,用于标识要跟踪的属性
  2. EditStateProperty - 一个用于保存属性状态数据的类
  3. EditStateStore - 一个用于保存被跟踪的 EditContext.Model 真实状态的集合类
  4. 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 中将所有内容连接在一起的组件。

组件

  1. 捕获 EditContext
  2. 创建一个 EditStateStore
  3. 将处理程序连接到 EditContextOnFieldChanged 事件。

OnFieldChanged 在存储上调用 Update,如果编辑状态发生更改,则调用 EditStateChanged

LockNavigation 启用/禁用导航锁定。如果需要,UI 添加 NavigationLock 组件并将其连接起来。

OnLocationChangedNavigationLock 的回调处理程序,用于在表单脏时阻止导航。

@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;
}

编辑表单

这是一个非常标准的编辑表单。 请注意

  1. 添加到 EditFormEditStateTracker 组件。
  2. 通过 EditStateTracker 上的 EditStateChanged 跟踪编辑状态,并使用它来更改按钮的状态。
  3. 包含验证以显示其有效性。
  4. 有一个模拟保存以演示如何实现它。
@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 &deg;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 &deg;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 基于保存的模型创建一个新的 EditContextEditForm 检测到新的 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日:初始版本
© . All rights reserved.