构建 Blazor 编辑表单





5.00/5 (14投票s)
如何构建管理状态的 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/。 有直接、内联和模态对话框版本。
表单退出
用户可以通过三种(受控的)方式退出表单
- 表单内导航 - 点击表单内的退出按钮。
- 应用程序内导航 - 点击表单外部导航栏中的链接,点击浏览器上的前进或后退按钮。
- 应用程序外导航 - 在地址栏中输入新 URL,点击收藏夹,关闭浏览器标签页或应用程序。
我们无法控制关闭浏览器(例如重启或系统崩溃),因此这里不予考虑。
表单编辑状态
在我们能够智能地控制编辑表单退出之前,我们需要了解表单的状态——表单中的数据是否与记录不同?开箱即用的 Blazor 没有提供实现此机制。 EditContext
中有一个非常简单的尝试,但它不符合目的。 我们需要一个编辑状态管理器。
此实现使用了两个主要类。
EditStateService
- 是一个作用域服务,用于在 SPA 会话期间保存当前编辑表单的状态。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
内,并通过依赖注入捕获级联的 EditContext
和 EditStateService
。它公开了一个 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
。它提供了
- 一个
IsDirty
属性,用于检查集合中所有EditFields
的状态。 - 一组用于添加和设置编辑状态的 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
初始化时,它会
- 从
EditContext.Model
加载EditFields
。 - 检查
EditStateService
,如果脏了则获取并反序列化Data
。 - 将每个
EditField
的EditedValue
设置为反序列化的Data
值。 - 将保存的
Data
值重新应用到EditContext.Model
。 - 将
FieldChanged
挂钩到EditContext
上的OnFieldChanged
以接收用户编辑。 - 将
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
由用户更改表单中的值触发。它
- 读取当前的
IsDirty
。 - 从
FieldChangedEventArgs
获取属性和新值。 - 在
EditFieldCollection
中设置EditField
。 - 检查编辑状态是否已更改,如果是,则调用
EditStateChanged
事件。 - 更新
EditStateService
编辑状态。如果编辑状态是脏的,则更新它;如果编辑状态是干净的,则清除它。 - 如果编辑状态发生更改,则设置/重置
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);
OnSave
清除当前编辑状态并从更新的model
重新加载EditFields
。NotifyEditStateChanged
通知EditStateService
编辑状态已更改。这会触发EditStateChanged
事件。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 直接使用 RenderFragments
和 RenderTreeBuilder
构建
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);
}
}
该组件有两个按钮事件处理程序来处理两种脏表单选项
DirtyExit
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;
更新 SaveRecord
和 Exit
以通知服务状态更改。
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 重新加载页面。
这次您会收到浏览器的挑战——文本取决于具体的浏览器——它们都以略微不同的方式实现挑战。
查看 内联编辑器