目录
介绍
示例项目、代码和链接
基本表单
RecordFormBase
EditRecordFormBase
实现表单
WeatherForecastViewerForm
WeatherForecastEditorForm
RouteView实现
总结
介绍这是关于如何在Blazor中构建和构建数据库应用程序的系列文章中的第三篇。到目前为止的文章是:
- 项目结构和框架——一些介绍。
- 服务——构建CRUD数据层。
- 查看组件——UI中的CRUD编辑和查看操作。
- UI组件——构建 HTML/CSS控件。
- 查看组件——UI中的CRUD列表操作。
本文详细介绍了构建可重用的CRUD表示层组件,特别是编辑和查看功能。自第一个版本以来发生了重大变化。
我发现有趣的是,大多数程序员尝试通过构建控件生成器而不是样板程序来自动化编辑和查看表单。大多数表单对于它们的记录集都是独一无二的。某些字段可以组合在一起并放在同一行上。文本字段的长度会根据需要的字符数而变化。建立一个工厂来处理这个问题,再加上控件、数据类实例和验证之间的链接的额外复杂性,似乎不值得。配置数据集变得比它试图模仿的形式更复杂。由于这些原因,这里没有表单构建器,只有一组用于标准化表单构建的库UI组件类。
示例项目、代码和链接文章的存储库已移至CEC.Database存储库。您可以将其用作开发您自己的应用程序的模板。以前的存储库已过时,将被删除。
存储库中的/SQL中有一个用于构建数据库的SQL脚本。该应用程序可以使用真正的SQL数据库或内存中的SQLite数据库。
您可以在同一站点上看到在此处运行的项目的Server和WASM版本。
表单中使用了几个自定义控件。有关这些的详细信息在单独的文章中进行了介绍:
- 编辑窗体状态控件
- 编辑验证状态控件
- 内联对话框控件
- 模态对话框控件
所有UI组件都继承自ComponentBase。所有源文件都可以在Github站点上查看,我在文章的适当位置包含了特定代码文件的引用或链接。在大多数地方,您需要通读代码以获取对功能的详细评论。
RecordFormBaseRecordFormBase是记录表单使用的基本抽象类。它继承自ComponentBase.。可以在多种上下文中创建记录表单:
- 作为RouteView中的根组件,RouteView通过参数Id传递表单。
- 在列表或其他组件内的模式对话框中。ID通过DialogOptions类传递给表单。
- 作为另一个组件(例如列表)中的内联编辑器,其中组件通过Id参数传递表单。
RecordFormBase旨在通过检查级联IModalDialog对象来检测它的上下文,特别是模态对话框上下文。有两组依赖:
- 记录的Id。如果表单是在RouteView或其他部件中托管的,它可以作为Parameter传递,或在IModalDialog的公共托管ModalOptions属性中传递。请注意,Id为-1表示新记录,而为0表示默认记录(0 是int的默认值)。
- 退出机制。这要么是:
- 如果它在模态上下文中,则调用Modal的close 。
- 如果已注册,则调用ExitAction委托。
- 默认——退出到root。
// Blazor.SPA/Components/Base/RecordFormBase.cs
public class RecordFormBase : ComponentBase where TRecord : class, IDbRecord, new()
{
[CascadingParameter] public IModalDialog Modal { get; set; }
[Parameter] public int ID {get; set;}
[Parameter] public EventCallback ExitAction { get; set; }
[Inject] protected NavigationManager NavManager { get; set; }
protected IFactoryControllerService Service { get; set; }
protected virtual bool IsLoaded => this.Service != null && this.Service.Record != null;
protected virtual bool HasServices => this.Service != null;
protected bool _isModal => this.Modal != null;
protected int _modalId = 0;
protected int _Id => _modalId != 0 ? _modalId : this.ID;
protected async override Task OnInitializedAsync()
{
await LoadRecordAsync();
await base.OnInitializedAsync();
}
protected virtual async Task LoadRecordAsync()
{
this.TryGetModalID();
await this.Service.GetRecordAsync(this._Id);
}
protected virtual bool TryGetModalID()
{
if (this._isModal && this.Modal.Options.TryGet("Id", out int value))
{
this._modalId = value;
return true;
}
return false;
}
protected virtual void Exit()
{
if (this._isModal)
this.Modal.Close(ModalResult.OK());
else if (ExitAction.HasDelegate)
ExitAction.InvokeAsync();
else
this.NavManager.NavigateTo("/");
}
}
EditRecordFormBase是编辑器表单的基础。它继承自RecordFormBase并实现了编辑功能。
它:
- 管理EditContext.
- 有一组布尔属性来跟踪状态和管理按钮显示/禁用状态。
- 保存记录。
当表单脏时,该Dirty属性与模态对话框连接以将其锁定(不允许退出和关闭导航)。
// Blazor.SPA/Components/Base/EditRecordFormBase.cs
public abstract class EditRecordFormBase : RecordFormBase, IDisposable where TRecord : class, IDbRecord, new()
{
/// Edit Context for the Editor - built from the service record
protected EditContext EditContext { get; set; }
/// Property tracking the Edit state of the form
protected bool IsDirty
{
get => this._isDirty;
set
{
if (value != this.IsDirty)
{
this._isDirty = value;
if (this._isModal) this.Modal.Lock(value);
}
}
}
/// model used by the Edit Context
protected TRecord Model => this.Service?.Record ?? null;
/// Reference to the form EditContextState control
protected EditFormState EditFormState { get; set; }
下一组属性是代码中使用的状态属性,以及用于控制显示/禁用状态的razor按钮。
protected bool _isNew => this.Service?.IsNewRecord ?? true;
protected bool _isDirty = false;
protected bool _isValid = true;
protected bool _saveDisabled => !this.IsDirty || !this._isValid;
protected bool _deleteDisabled => this._isNew || this._confirmDelete;
protected bool _isLoaded = false;
protected bool _dirtyExit = false;
protected bool _confirmDelete = false;
protected bool _isInlineDirty => (!this._isModal) && this._isDirty;
protected string _saveButtonText => this._isNew ? "Save" : "Update";
LoadRecordAsync调用base以获取记录,创建EditContext并注册EditContext.OnFieldChanged.。其他方法处理状态变化。
protected async override Task OnInitializedAsync()
=> await LoadRecordAsync();
/// Method to load the record
/// calls the base method to load the record and then sets up the EditContext
protected override async Task LoadRecordAsync()
{
await base.OnInitializedAsync();
this.EditContext = new EditContext(this.Model);
_isLoaded = true;
this.EditContext.OnFieldChanged += FieldChanged;
if (!this._isNew)
this.EditContext.Validate();
}
/// Event handler for EditContext OnFieldChanged Event
protected void FieldChanged(object sender, FieldChangedEventArgs e)
{
this._dirtyExit = false;
this._confirmDelete = false;
}
/// Method to change edit state
protected void EditStateChanged(bool dirty)
=> this.IsDirty = dirty;
/// Method to change the Validation state
protected void ValidStateChanged(bool valid)
=> this._isValid = valid;
/// IDisposable Interface Implementation
public void Dispose()
=> this.EditContext.OnFieldChanged -= FieldChanged;
最后是按钮事件处理程序来控制保存和退出脏表单。
/// Method to handle EditForm submission
protected async void HandleValidSubmit()
{
await this.Service.SaveRecordAsync();
this.EditFormState.UpdateState();
this._dirtyExit = false;
await this.InvokeAsync(this.StateHasChanged);
}
/// Handler for Delete action
protected void Delete()
{
if (!this._isNew)
this._confirmDelete = true;
}
/// Handler for Delete confirmation
protected async void ConfirmDelete()
{
if (this._confirmDelete)
{
await this.Service.DeleteRecordAsync();
this.IsDirty = false;
this.DoExit();
}
}
/// Handler for a confirmed exit - i.e. dirty exit
protected void ConfirmExit()
{
this.IsDirty = false;
this.DoExit();
}
/// Handler to Exit the form, dependant on it context
protected void DoExit(ModalResult result = null)
{
result = result ?? ModalResult.OK();
if (this._isModal)
this.Modal.Close(result);
if (ExitAction.HasDelegate)
ExitAction.InvokeAsync();
else
this.NavManager.NavigateTo("/");
}
}
WeatherForecastViewerForm的代码非常简单。
- 继承自RecordFormBase并设置TRecord为WeatherForecast.
- 获取WeatherForecastControllerService并将其分配给基本Service属性。
public partial class WeatherForecastViewerForm : RecordFormBase
{
[Inject] private WeatherForecastControllerService ControllerService { get; set; }
protected async override Task OnInitializedAsync()
{
this.Service = this.ControllerService;
await base.OnInitializedAsync();
}
}
大部分工作都在Razor代码中。
- 没有Html代码,都是组件。我们将在下一篇文章中详细介绍UI组件。
- 布局基于Bootstrap网格。
- 列大小决定控件大小。
- 仅当我们有要显示的记录时才加载UILoader 的内容。
@namespace Blazor.Database.Components
@inherits RecordFormBase
Weather Forecast Viewer
Date
Temperature °C
Temperature °f
Summary
Exit
WeatherForecastEditorForm类似于WeatherForecastViewerForm。
代码再次非常简单。
- 继承自EditRecordFormBase并设置TRecord为WeatherForecast.
- 获取WeatherForecastControllerService并将其分配给基本Service属性。
public partial class WeatherForecastViewerForm : RecordFormBase
{
[Inject] private WeatherForecastControllerService ControllerService { get; set; }
protected async override Task OnInitializedAsync()
{
this.Service = this.ControllerService;
await base.OnInitializedAsync();
}
}
Razor文件如下所示。它基于带有一些附加控件的标准Blazor EditForm。在查看器上发表的相同评论也适用于此处。此外:
- InlineDialog是一个表单锁定控件。它由_isInlineDirty属性启用。转到演示站点并编辑记录以查看其实际效果。它仅在表单不在模态上下文中时启用。
- EditFormState是一个跟踪表单状态的控件,即当表单加载时记录与原始记录的状态。它链接InlineDialog到控制表单锁定。
- ValidationFormState 是一个自定义验证控件。
- 按钮与布尔控件属性相关联以管理其状态。
自定义控件在链接部分中引用的单独文章中进行了介绍。我没有进一步抽象,所以你可以看到一个完整的表格。您可以将所有标题、编辑表单和按钮部分移动到FormWrapper组件中。
@namespace Blazor.Database.Components
@inherits EditRecordFormBase
Weather Forecast Editor
Record ID
this.Model.Date) />
Date
this.Model.Date) />
Temperature °C
this.Model.TemperatureC) />
Summary
this.Model.Summary) />
Delete
Confirm Delete
@this._saveButtonText
Exit Without Saving
Exit
查看器的RouteView实现如下所示。
- 用ID Parameter声明Route。
- 声明表单WeatherForecastViewerForm。
- 将 ID传递给表单并附加一个委托ExitAction,该委托将返回到fetchdata视图。
// WeatherViewer.razor
@page "/weather/view/{ID:int}"
@code {
[Parameter] public int ID { get; set; }
[Inject] public NavigationManager NavManager { get; set; }
private void ExitToList()
=> this.NavManager.NavigateTo("/fetchdata");
}
编辑器完全相同,但声明了表单WeatherForecastEditorForm。
// WeatherEditor.razor
@page "/weather/edit/{ID:int}"
@code {
[Inject] public NavigationManager NavManager { get; set; }
[Parameter] public int ID { get; set; }
private void ExitToList()
=> this.NavManager.NavigateTo("/fetchdata");
}
这篇文章到此结束。我们已经展示了如何将样板代码构建到基本表单中以及如何实现查看器和编辑器表单。我们将在单独的文章中更详细地介绍列表形式以及如何调用查看器和编辑器。
需要注意的一些关键点:
- Blazor服务器和Blazor WASM代码是相同的。
- 几乎所有功能都在库组件中实现。大多数应用程序代码是各个记录字段的Razor标记。
- Razor文件包含控件,而不是HTML。
- 通过使用异步。
如果您在未来阅读本文,请查看存储库中的自述文件以获取文章集的最新版本。
Building a Database Application in Blazor - Part 3 - CRUD Edit and View Operations in the UI - CodeProject