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

构建 Blazor 编辑表单

starIconstarIconstarIconstarIconstarIcon

5.00/5 (14投票s)

2021 年 2 月 10 日

CPOL

8分钟阅读

viewsIcon

22572

如何构建管理状态的 Blazor 编辑表单

为了给本文设定背景,自 Blazor 发布以来,关于如何处理编辑表单,尤其是如何阻止或至少警告用户在离开脏表单时,已经有很多讨论、文章和提议。这个问题并非 Blazor 特有:所有单页应用程序和网站都面临同样的挑战。

在经典的 Web 表单中,每次导航都是对服务器的 Get 或 Post 回调。我们可以使用浏览器 window.beforeunload 事件来警告用户页面上存在未保存的数据。这虽然不太理想,但至少聊胜于无——我们稍后会用到它。这种技术在 SPA 中行不通。对于外部观察者来说,看起来像是导航事件的,实际上并非如此。NavigationManager 拦截页面上的任何导航尝试,触发其自己的 LocationChanged 事件并终止请求。路由器(已连接到此事件)会施展其魔法,将新的组件集加载到页面中。没有真正的浏览器导航发生,因此浏览器的 beforeunload 事件无法捕获任何内容。

程序员需要编写代码来阻止用户离开脏表单。当你的应用程序依赖于 URL 导航前提时,这说起来容易做起来难。工具栏、导航侧边栏和许多按钮都会提交 URL 以在应用程序中导航。想想开箱即用的 Blazor 模板。左侧导航中有所有链接,顶部栏中也有关于链接。

我个人对整个路由的伪装有严重的异议:SPA 是一个应用程序,而不是一个网站,但我认为我可能是一个少数派!本文是为多数派而写的。

我遇到的所有 Blazor 编辑状态解决方案都或多或少存在缺陷,我自己也创建过不止一个。社区希望 NetCore 5 能有所改变,特别是 NavigationManager 中能增加一些额外功能,以取消或阻止导航请求。但这并没有发生:我认为团队在正确的解决方案上没有达成共识,所以我们又回到了原点。

本文介绍的是我解决这个问题的最新方法。它并不完美,但我认为除非我们得到一些新的浏览器标准,允许切换到 SPA 模式并控制工具栏导航,否则我们永远不会得到一个接近完美的解决方案。

我们的目标是,如果用户试图退出一个脏表单,就以两种方式之一阻止他们。不允许开侧门!

代码仓库和演示网站

本文的仓库在此

你可以在我的 Blazr 数据库演示网站上看到本文中的代码实际运行效果,网址是 - https://cec-blazor-database.azurewebsites.net/。 有直接、内联和模态对话框版本。

表单退出

用户可以通过三种(受控的)方式退出表单

  1. 表单内导航 - 点击表单内的退出按钮。
  2. 应用程序内导航 - 点击表单外部导航栏中的链接,点击浏览器上的前进或后退按钮。
  3. 应用程序外导航 - 在地址栏中输入新 URL,点击收藏夹,关闭浏览器标签页或应用程序。

我们无法控制关闭浏览器(例如重启或系统崩溃),因此这里不予考虑。

表单编辑状态

在我们能够智能地控制编辑表单退出之前,我们需要了解表单的状态——表单中的数据是否与记录不同?开箱即用的 Blazor 没有提供实现此机制。 EditContext 中有一个非常简单的尝试,但它不符合目的。 我们需要一个编辑状态管理器。

此实现使用了两个主要类。

  1. EditStateService - 是一个作用域服务,用于在 SPA 会话期间保存当前编辑表单的状态。
  2. EditFormState - 是一个与表单内的 EditContext 交互的组件。它将初始 Model 值存储在 EditFieldCollection 中,接收来自 EditContext 的更新,并在发生更改时更新 EditStateService

EditStateService

EditStateService 是一个作用域服务状态容器,用于跟踪表单的编辑状态。它有一组设置和更新状态的方法,以及两个事件。

using System;

namespace Blazr.EditForms
{
    /// <summary>
    /// Service Class for managing Form Edit State
    /// </summary>
    public class EditStateService
    {
        private bool _isDirty;

        public bool IsDirty => _isDirty && !string.IsNullOrWhiteSpace(this.Data) && !string.IsNullOrWhiteSpace(this.Data);
        public string Data { get; set; }
        public string EditFormUrl { get; set; }
        public bool ShowEditForm => (!String.IsNullOrWhiteSpace(EditFormUrl)) && IsDirty;
        public bool DoFormReload { get; set; }

        public event EventHandler RecordSaved;
        public event EventHandler<EditStateEventArgs> EditStateChanged;

        public void SetEditState(string data, string formUrl)
        {
            this.Data = data;
            this.EditFormUrl = formUrl;
            this._isDirty = true;
        }

        public void ClearEditState()
        {
            this.Data = null;
            this._isDirty = false;
            this.EditFormUrl = string.Empty;
        }

        public void ResetEditState()
        {
            this.Data = null;
            this._isDirty = false;
            this.EditFormUrl = string.Empty;
        }

        public void NotifyRecordSaved()
        {
            RecordSaved?.Invoke(this, EventArgs.Empty);
            EditStateChanged?.Invoke(this, EditStateEventArgs.NewArgs(false));
        }

        public void NotifyRecordExit()
            => this.NotifyRecordSaved();

        public void NotifyEditStateChanged(bool dirtyState)
            => EditStateChanged?.Invoke(this, EditStateEventArgs.NewArgs(dirtyState));
    }
}

EditStateEventArgs

using System;

namespace Blazr.EditForms
{
    public class EditStateEventArgs : EventArgs
    {
        public bool IsDirty { get; set; }

        public static EditStateEventArgs NewArgs(bool dirtyState)
            => new EditStateEventArgs { IsDirty = dirtyState };
    }
}

EditFormState

EditFormState 是一个没有 UI 输出的 UI 控件。它放置在 EditForm 内,并通过依赖注入捕获级联的 EditContextEditStateService。它公开了一个 EditStateChanged 事件和一个 IsDirty 属性。

EditFormState 读取 EditContext 的所有写入属性并将它们保存到 EditFields 集合中。

EditField

EditField 看起来像这样。除 EditedValue 外,所有都是 init 记录类型属性。

    public class EditField
    {
        public string FieldName { get; init; }
        public Guid GUID { get; init; }
        public object Value { get; init; }
        public object EditedValue { get; set; }
        public object Model { get; init; }

        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 实现了 IEnumerable。它提供了

  1. 一个 IsDirty 属性,用于检查集合中所有 EditFields 的状态。
  2. 一组用于添加和设置编辑状态的 getter 和 setter。
    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());
.....  lots of getters and setters and IEnumerator implementation code

EditFormState

EditFormState 属性/字段

public class EditFormState : ComponentBase, IDisposable
{
    private bool disposedValue;
    private EditFieldCollection EditFields = new EditFieldCollection();

    [CascadingParameter] public EditContext EditContext { get; set; }

    [Inject] private EditStateService EditStateService { get; set; }
    [Inject] private IJSRuntime _js { get; set; }
    [Inject] private NavigationManager NavManager { get; set; }

EditFormState 初始化时,它会

  1. EditContext.Model 加载 EditFields
  2. 检查 EditStateService,如果脏了则获取并反序列化 Data
  3. 将每个 EditFieldEditedValue 设置为反序列化的 Data 值。
  4. 将保存的 Data 值重新应用到 EditContext.Model
  5. FieldChanged 挂钩到 EditContext 上的 OnFieldChanged 以接收用户编辑。
  6. OnSave 挂钩到 EditStateService 上的 RecordSaved 以了解何时重置。
protected override Task OnInitializedAsync()
{
    Debug.Assert(this.EditContext != null);

    if (this.EditContext != null)
    {
        // Populates the EditField Collection
        this.LoadEditState();
        // Wires up to the EditContext OnFieldChanged event
        this.EditContext.OnFieldChanged += this.FieldChanged;
        this.EditStateService.RecordSaved += this.OnSave;
    }
    return Task.CompletedTask;
}

private void LoadEditState()
{
    this.GetEditFields();
    if (EditStateService.IsDirty)
        SetEditState();
}

private void GetEditFields()
{
    var model = this.EditContext.Model;
    this.EditFields.Clear();
    if (model is not null)
    {
        var props = model.GetType().GetProperties();
        foreach (var prop in props)
        {
            if (prop.CanWrite)
            {
                var value = prop.GetValue(model);
                EditFields.AddField(model, prop.Name, value);
            }
        }
    }
}

private void SetEditState()
{
    var recordtype = this.EditContext.Model.GetType();
    object data = JsonSerializer.Deserialize(EditStateService.Data, recordtype);
    if (data is not null)
    {
        var props = data.GetType().GetProperties();
        foreach (var property in props)
        {
            var value = property.GetValue(data);
            EditFields.SetField(property.Name, value);
        }
        this.SetModelToEditState();
        if (EditFields.IsDirty)
            this.NotifyEditStateChanged();
    }
}

private void SetModelToEditState()
{
    var model = this.EditContext.Model;
    var props = model.GetType().GetProperties();
    foreach (var property in props)
    {
        var value = EditFields.GetEditValue(property.Name);
        if (value is not null && property.CanWrite)
            property.SetValue(model, value);
    }
}

FieldChanged 由用户更改表单中的值触发。它

  1. 读取当前的 IsDirty
  2. FieldChangedEventArgs 获取属性和新值。
  3. EditFieldCollection 中设置 EditField
  4. 检查编辑状态是否已更改,如果是,则调用 EditStateChanged 事件。
  5. 更新 EditStateService 编辑状态。如果编辑状态是脏的,则更新它;如果编辑状态是干净的,则清除它。
  6. 如果编辑状态发生更改,则设置/重置 PageExitCheck——稍后会详细介绍。
private void FieldChanged(object sender, FieldChangedEventArgs e)
{
    var wasDirty = EditFields?.IsDirty ?? false;
    // 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 if changed
        var isStateChange = (EditFields?.IsDirty ?? false) != wasDirty;
        var isDirty = EditFields?.IsDirty ?? false;
        if (isStateChange)
            this.NotifyEditStateChanged();
        if (isDirty)
            this.SaveEditState(isStateChange);
        else
            this.ClearEditState();
    }
}

private void SaveEditState(bool isStateChange)
{
    if (isStateChange)
        this.SetPageExitCheck(true);
    var jsonData = JsonSerializer.Serialize(this.EditContext.Model);
    EditStateService.SetEditState(jsonData, NavManager.Uri);
}

private void ClearEditState()
{
    this.SetPageExitCheck(false);
    EditStateService.ClearEditState();
}

private void SetPageExitCheck(bool action)
    => _js.InvokeAsync<bool>("blazr_setEditorExitCheck", action);
  1. OnSave 清除当前编辑状态并从更新的 model 重新加载 EditFields
  2. NotifyEditStateChanged 通知 EditStateService 编辑状态已更改。这会触发 EditStateChanged 事件。
  3. Dispose 清理资源。
private void OnSave(object sender, EventArgs e)
{
    this.ClearEditState();
    this.LoadEditState();
}

private void NotifyEditStateChanged()
{
    var isDirty = EditFields?.IsDirty ?? false;
    this.EditStateService.NotifyEditStateChanged(isDirty);
}

// IDisposable Implementation
protected virtual void Dispose(bool disposing)
{
    if (!disposedValue)
    {
        if (disposing)
        {
            if (this.EditContext != null)
                this.EditContext.OnFieldChanged -= this.FieldChanged;
        }
        this.EditStateService.RecordSaved -= this.OnSave;
        disposedValue = true;
    }
}

public void Dispose()
{
    // Do not change this code.  Put cleanup code in 'Dispose(bool disposing)' method
    Dispose(disposing: true);
    GC.SuppressFinalize(this);
}

站点外导航

这发生在用户尝试离开站点时——关闭浏览器标签页,或点击收藏夹。在 Blazor 中没有办法直接阻止这种情况——没有触发的事件可以链接。但是,浏览器确实有一个 beforeunload 事件。你对其控制不多,但可以告诉浏览器询问用户是否希望退出页面。

site.js 定义了两个函数,用于从浏览器 window 对象添加/移除事件。

window.blazr_setEditorExitCheck = function (show) {
    if (show) {
        window.addEventListener("beforeunload", blazr_showExitDialog);
    }
    else {
        window.removeEventListener("beforeunload", blazr_showExitDialog);
    }
}

window.blazr_showExitDialog = function (event) {
    event.preventDefault();
    event.returnValue = "There are unsaved changes on this page.   Do you want to leave?";
}

这可以从 Blazor 调用

[Inject] private IJSRuntime _js { get; set; }

private void SetPageExitCheck(bool action)
    => _js.InvokeAsync<bool>("blazr_setEditorExitCheck", action);

SetPageExitCheck 用于在 EditFormState 中设置和清除编辑状态

private void SaveEditState(bool isStateChange)
{
    if (isStateChange)
        this.SetPageExitCheck(true);
    var jsonData = JsonSerializer.Serialize(this.EditContext.Model);
    EditStateService.SetEditState(jsonData, NavManager.Uri);
}

private void ClearEditState()
{
    this.SetPageExitCheck(false);
    EditStateService.ClearEditState();
}

并在 WeatherEditor 中用于在退出表单时清除编辑状态。

private void Exit()
{
    this.EditStateService.ResetEditState();
    this.SetPageExitCheck(false);
    NavManager.NavigateTo("/fetchdata");
}

站点内导航

站点内导航由 App 中定义的 Router 处理。实际渲染由 RouteView 处理。这是一个比路由器更简单的可修改组件。我们修订后的 RouteView 流程如下所示

RouteViewManager

RouteViewManager 基于 RouteView。大部分代码直接从该组件中提取。没有 Razor 代码,HTML 直接使用 RenderFragmentsRenderTreeBuilder 构建

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.JSInterop;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;

namespace Blazr.EditForms
{
    public class RouteViewManager : IComponent
    {
        private bool _RenderEventQueued;
        private RenderHandle _renderHandle;

        [Inject] private EditStateService EditStateService { get; set; }

        [Inject] private IJSRuntime _js { get; set; }

        [Inject] private NavigationManager NavManager { get; set; }

        [Parameter] public RouteData RouteData { get; set; }

        [Parameter] public Type DefaultLayout { get; set; }

        public void Attach(RenderHandle renderHandle)
            => _renderHandle = renderHandle;

        public async Task SetParametersAsync(ParameterView parameters)
        {
            // Sets the component parameters
            parameters.SetParameterProperties(this);

            // Check if we have either RouteData or ViewData
            if (RouteData == null)
            {
                throw new InvalidOperationException($"The {nameof(RouteView)} component requires a non-null value for the parameter {nameof(RouteData)}.");
            }
            // Render the component
            await this.RenderAsync();
        }

        private RenderFragment _renderDelegate => builder =>
        {
            _RenderEventQueued = false;
            // Adds cascadingvalue for the ViewManager
            builder.OpenComponent<CascadingValue<RouteViewManager>>(0);
            builder.AddAttribute(1, "Value", this);
            // Get the layout render fragment
            builder.AddAttribute(2, "ChildContent", this._layoutViewFragment);
            builder.CloseComponent();
        };

        private RenderFragment _layoutViewFragment => builder =>
        {
            Type _pageLayoutType = RouteData?.PageType.GetCustomAttribute<LayoutAttribute>()?.LayoutType
                ?? DefaultLayout;

            builder.OpenComponent<LayoutView>(0);
            builder.AddAttribute(1, nameof(LayoutView.Layout), _pageLayoutType);
            if (this.EditStateService.IsDirty && this.EditStateService.DoFormReload is not true)
                builder.AddAttribute(2, nameof(LayoutView.ChildContent), _dirtyExitFragment);
            else
            {
                this.EditStateService.DoFormReload = false;
                builder.AddAttribute(3, nameof(LayoutView.ChildContent), _renderComponentWithParameters);
            }
            builder.CloseComponent();
        };

        private RenderFragment _dirtyExitFragment => builder =>
        {
            builder.OpenElement(0, "div");
            builder.AddAttribute(1, "class", "dirty-exit");
            {
                builder.OpenElement(2, "div");
                builder.AddAttribute(3, "class", "dirty-exit-message");
                builder.AddContent(4, "You are existing a form with unsaved data");
                builder.CloseElement();
            }
            {
                builder.OpenElement(5, "div");
                builder.AddAttribute(6, "class", "dirty-exit-message");
                {
                    builder.OpenElement(7, "button");
                    builder.AddAttribute(8, "class", "dirty-exit-button");
                    builder.AddAttribute(9, "onclick", EventCallback.Factory.Create<MouseEventArgs>(this, this.DirtyExit));
                    builder.AddContent(10, "Exit and Clear Unsaved Data");
                    builder.CloseElement();
                }
                {
                    builder.OpenElement(11, "button");
                    builder.AddAttribute(12, "class", "load-dirty-form-button");
                    builder.AddAttribute(13, "onclick", EventCallback.Factory.Create<MouseEventArgs>(this, this.LoadDirtyForm));
                    builder.AddContent(14, "Reload Form");
                    builder.CloseElement();
                }
                builder.CloseElement();
            }
            builder.CloseElement();
        };

        private RenderFragment _renderComponentWithParameters => builder =>
        {
            Type componentType = null;
            IReadOnlyDictionary<string, object> parameters = new Dictionary<string, object>();

            if (RouteData != null)
            {
                componentType = RouteData.PageType;
                parameters = RouteData.RouteValues;
            }

            if (componentType != null)
            {
                builder.OpenComponent(0, componentType);
                foreach (var kvp in parameters)
                {
                    builder.AddAttribute(1, kvp.Key, kvp.Value);
                }
                builder.CloseComponent();
            }
            else
            {
                builder.OpenElement(0, "div");
                builder.AddContent(1, "No Route or View Configured to Display");
                builder.CloseElement();
            }
        };

        public async Task RenderAsync() => await InvokeAsync(() =>
        {
            if (!this._RenderEventQueued)
            {
                this._RenderEventQueued = true;
                _renderHandle.Render(_renderDelegate);
            }
        }
        );

        protected Task InvokeAsync(Action workItem)
            => _renderHandle.Dispatcher.InvokeAsync(workItem);

        protected Task InvokeAsync(Func<Task> workItem)
            => _renderHandle.Dispatcher.InvokeAsync(workItem);

        private Task DirtyExit(MouseEventArgs d)
        {
            this.EditStateService.ClearEditState();
            this.SetPageExitCheck(false);
            return RenderAsync();
        }

        private void LoadDirtyForm(MouseEventArgs e)
        {
            this.EditStateService.DoFormReload = true;
            NavManager.NavigateTo(this.EditStateService.EditFormUrl);
        }

        private void SetPageExitCheck(bool action)
            => _js.InvokeAsync<bool>("cecblazor_setEditorExitCheck", action);
    }
}

该组件有两个按钮事件处理程序来处理两种脏表单选项

  1. DirtyExit
  2. LoadDirtyForm

以及 SetPageExitCheck 来设置浏览器页面退出事件。

RenderFragement 代码构建布局,该布局添加 _dirtyExitFragment 以构建 Dirty Exit 视图,或添加 _renderComponentWithParameters 以构建路由/视图组件。

添加 CSS

将以下 Css 添加到其中一个引用的 Css 文件中。在解决方案中,它位于库项目中的独立 site.css 中。

div.dirty-exit {
    width: 400px;
    margin: 10px auto 10px auto;
}

div.dirty-exit-message {
    text-align: center;
    margin: 20px 0px;
    font-size: 1.5rem;
    font-weight: 600;
    font-variant: small-caps;
}

div.dirty-exit button {
    display: inline-block;
    font-size: 1rem;
    font-weight: 400;
    color: #fff;
    vertical-align: middle;
    padding: .4rem .75rem;
    text-align: center;
    margin-right: 1rem;
    border-radius: 0;
    border-style: none;
}

button.dirty-exit-button {
    background-color: #e74a3b;
    border-color: #e74a3b;
}

button.load-dirty-form-button {
    background-color: #1cc88a;
    border-color: #1cc88a;
}

实现解决方案

解决方案

从服务器模板创建一个 Blazor 解决方案。

  • 解决方案名称:Blazr.EditForms
  • 项目名称:Blazr.EditForms.Server

添加一个 Razor 库模板项目 - Blazr.EditForms。从我的仓库中复制所有代码。

Blazr.EditForms.Server

更新 Startup

public void ConfigureServices(IServiceCollection services)
{
    ....
    services.AddSingleton<WeatherForecastService>();
    // Add the EditStateService
    services.AddScoped<EditStateService>();
}

更新 WeatherForecastService

// Add a method to get a dummy record
public Task<WeatherForecast> GetWeatherForecastAsync(Guid Id)
{
    return Task.FromResult(new WeatherForecast
    {
        Date = DateTime.Now,
        TemperatureC = 12,
        Summary = "Balmy"
    });
}

更新 _Hosts.cshtml

<head>
    // extra stylesheets
    <link href="/_content/Blazr.EditForms/site.css" rel="stylesheet" />
    <link href="/Blazr.EditForms.Server.styles.css" rel="stylesheet" />
</head>
.....
    // site Js
    <script src="/_content/Blazr.EditForms/site.js"></script>
</body>

更新 NavMenu

// Add two more links
<li class="nav-item px-3">
    <NavLink class="nav-link" href="WeatherForecastEditor">
        <span class="oi oi-list-rich" aria-hidden="true"></span> Editor
    </NavLink>
</li>
<li class="nav-item px-3">
    <NavLink class="nav-link" href="InlineWeatherForecastEditor">
        <span class="oi oi-list-rich" aria-hidden="true"></span> Inline Editor
    </NavLink>
</li>

更新 App.razor,将 RouteView 替换为 RouteViewManager

....
<Found Context="routeData">
    <RouteViewManager RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
....

WeatherForecastEditor

我们的基本编辑器表单如下所示。它本身就可以工作,但没有编辑状态和退出/导航控制。

@page "/WeatherForecastEditor"

@using Blazr.EditForms.Server.Data
@implements IDisposable

<div class="container">
    <div class="row">
        <div class="col-12">
            <h3>Weather Forecast Editor</h3>
        </div>
    </div>
    <EditForm Model="record" OnValidSubmit="SaveRecord">
        <div class="row">
            <div class="col-12">
                <label class="form-label">Date</label>
                <InputDate class="form-control" @bind-Value="record.Date" />
                <div class="valid-feedback">
                    <ValidationMessage For="() => record.Date" />
                </div>
            </div>
        </div>
        <div class="row">
            <div class="col-12">
                <label class="form-label">Temperature</label>
                <InputNumber class="form-control" @bind-Value="record.TemperatureC" />
                <div class="valid-feedback">
                    <ValidationMessage For="() => record.TemperatureC" />
                </div>
            </div>
        </div>
        <div class="row">
            <div class="col-12">
                <label class="form-label">Summary</label>
                <InputText class="form-control" @bind-Value="record.Summary" />
                <div class="valid-feedback">
                    <ValidationMessage For="() => record.Summary" />
                </div>
            </div>
        </div>
        <div class="row mt-2">
            <div class="col-12 text-right">
                <button class="btn btn-success">Submit</button>
                <button class="btn btn-dark" @onclick="Exit">Exit</button>
            </div>
        </div>
    </EditForm>
</div>
@code {
    [Inject] WeatherForecastService ForecastService { get; set; }
    [Inject] NavigationManager NavManager { get; set; }

    private WeatherForecast record = new WeatherForecast();

    protected override async Task OnInitializedAsync()
    {
        EditService.EditStateChanged += OnEditStateChanged;
    }

    protected Task SaveRecord()
    {
        return Task.CompletedTask;
    }

    protected void Exit()
    {
        NavManager.NavigateTo("/");
    }

    public void Dispose()
    =>  EditService.EditStateChanged -= OnEditStateChanged;
}

添加编辑状态控制

EditFormState 控件添加到编辑表单。

    <EditForm Model="record" OnValidSubmit="SaveRecord">
        <EditFormState />
        ....

更新按钮,为其显示和启用状态添加一些状态控制。

<div class="col-12 text-right">
    @if (_isDirty)
    {
        <button class="btn btn-danger" @onclick="Exit">Exit without Saving</button>
    }
    <button class="btn btn-success" disabled="@_isClean">Submit</button>
    <button class="btn btn-dark" disabled="@_isDirty" @onclick="Exit">Exit</button>
</div>

注入 EditStateService 并添加内部字段以保存编辑状态

    // inject the EditStateService
    [Inject] EditStateService EditService { get; set; }

    // internal bool fields for state management
    private bool _isDirty;
    private bool _isClean => !_isDirty;

添加 OnEditStateChanged 事件处理程序并将其附加到 EditService.EditStateChanged。它设置内部状态字段并调用 StateHasChanged 以启动表单的渲染。在 Dispose 中注销事件处理程序。

protected override async Task OnInitializedAsync()
{
    this.record = await ForecastService.GetWeatherForecastAsync(Guid.NewGuid());
    EditService.EditStateChanged += OnEditStateChanged;
}

private void OnEditStateChanged(object sender, EditStateEventArgs e)
{
    _isDirty = e.IsDirty;
    StateHasChanged();
}

public void Dispose()
    =>  EditService.EditStateChanged -= OnEditStateChanged;

更新 SaveRecordExit 以通知服务状态更改。

protected Task SaveRecord()
{
    EditService.NotifyRecordSaved();
    return Task.CompletedTask;
}

protected void Exit()
{
    EditService.NotifyRecordExit();
    NavManager.NavigateTo("/");
}

使用内联对话框

内联对话框增加了更多控制,阻止了除浏览器后退/前进按钮之外的所有应用程序内导航。你可以在仓库中查看代码。它使用起来很简单。

添加一个 InlineWeatherForecastEditor 页面并复制 WeatherForecastEditor 的内容。

在整个表单周围添加一个 InLineDialog 控件包装器,并按所示设置参数。Lock 用于启用和禁用它。

@page "/InlineWeatherForecastEditor"
....
<InlineDialog Transparent="false" Lock="_isDirty">
    <div class="container p-2">
        ....
    </div>
</InlineDialog>

解决方案实战

运行解决方案并进入 Editor。您将看到

更改一个值,您将看到

点击菜单链接,或点击浏览器后退按钮

现在你收到来自 RouteViewManager 的脏退出挑战。检查每个操作会发生什么。

最后按下 F5 重新加载页面。

这次您会收到浏览器的挑战——文本取决于具体的浏览器——它们都以略微不同的方式实现挑战。

查看 内联编辑器

 

© . All rights reserved.