目录
介绍
这是有关如何在Blazor中构建和构造真正的数据库应用程序的系列文章中的第三篇。
- 项目结构与框架
- 服务——构建CRUD数据层
- View组件——UI中的CRUD编辑和查看操作
- UI组件——构建HTML / CSS控件
- View组件-UI中的CRUD列表操作
- 逐步详细介绍如何向应用程序添加气象站和气象站数据
本文详细介绍了如何构建可重用的CRUD表示层组件,尤其是“编辑”和“查看”功能——并将其用于Server和WASM项目。
示例项目和代码
存储库中有一个SQL脚本在/SQL中,用于构建数据库。
您可以在此处查看运行的项目的服务器版本。
你可以看到该项目的WASM版本运行在这里。
基本表单
所有CRUD UI组件都继承自Component
。文章中并没有显示所有代码:有些类太大了,无法显示所有内容。可以在Github站点上查看所有源文件,并且在本文的适当位置提供了对特定代码文件的引用或链接。许多信息详细信息在代码部分的注释中。
表单库
所有表格都继承自FormBase
。FormBase
提供以下功能:
- 从
OwningComponentBase
复制源代码以实现作用域服务管理 - 如果启用了身份验证,则获取用户
- 在模式或非模式状态下管理表单关闭
- 实现
IForm
和IDisposable
接口
作用域管理代码如下所示。您可以在Internet上搜索有关如何使用OwningComponentBase
的文章。
// CEC.Blazor/Components/Base/BaseForm.cs
private IServiceScope _scope;
/// Scope Factory to manage Scoped Services
[Inject] protected IServiceScopeFactory ScopeFactory { get; set; } = default!;
/// Gets the scoped IServiceProvider that is associated with this component.
protected IServiceProvider ScopedServices
{
get
{
if (ScopeFactory == null) throw new InvalidOperationException
("Services cannot be accessed before the component is initialized.");
if (IsDisposed) throw new ObjectDisposedException(GetType().Name);
_scope ??= ScopeFactory.CreateScope();
return _scope.ServiceProvider;
}
}
IDisposable
接口实现与作用域服务管理绑定在一起。稍后我们将使用它来删除事件处理程序。
protected bool IsDisposed { get; private set; }
/// IDisposable Interface
async void IDisposable.Dispose()
{
if (!IsDisposed)
{
_scope?.Dispose();
_scope = null;
Dispose(disposing: true);
await this.DisposeAsync(true);
IsDisposed = true;
}
}
/// Dispose Method
protected virtual void Dispose(bool disposing) { }
/// Async Dispose event to clean up event handlers
public virtual Task DisposeAsync(bool disposing) => Task.CompletedTask;
其余属性为:
[CascadingParameter] protected IModal ModalParent { get; set; }
/// Boolean Property to check if this component is in Modal Mode
public bool IsModal => this.ModalParent != null;
/// Cascaded Authentication State Task from CascadingAuthenticationState in App
[CascadingParameter] public Task<AuthenticationState> AuthenticationStateTask { get; set; }
/// Cascaded ViewManager
[CascadingParameter] public ViewManager ViewManager { get; set; }
/// Check if ViewManager exists
public bool IsViewManager => this.ViewManager != null;
/// Property holding the current user name
public string CurrentUser { get; protected set; }
/// Guid string for user
public string CurrentUserID { get; set; }
/// UserName without the domain name
public string CurrentUserName => (!string.IsNullOrEmpty(this.CurrentUser))
&& this.CurrentUser.Contains("@") ? this.CurrentUser.Substring
(0, this.CurrentUser.IndexOf("@")) : string.Empty;
主要事件方法:
/// OnRenderAsync Method from Component
protected async override Task OnRenderAsync(bool firstRender)
{
if (firstRender) await GetUserAsync();
await base.OnRenderAsync(firstRender);
}
/// Method to get the current user from the Authentication State
protected async Task GetUserAsync()
{
if (this.AuthenticationStateTask != null)
{
var state = await AuthenticationStateTask;
// Get the current user
this.CurrentUser = state.User.Identity.Name;
var x = state.User.Claims.ToList().FirstOrDefault
(c => c.Type.Contains("nameidentifier"));
this.CurrentUserID = x?.Value ?? string.Empty;
}
}
最后是退出按钮的方法。
public void Exit(ModalResult result)
{
if (IsModal) this.ModalParent.Close(result);
else this.ViewManager.LoadViewAsync(this.ViewManager.LastViewData);
}
public void Exit()
{
if (IsModal) this.ModalParent.Close(ModalResult.Exit());
else this.ViewManager.LoadViewAsync(this.ViewManager.LastViewData);
}
public void Cancel()
{
if (IsModal) this.ModalParent.Close(ModalResult.Cancel());
else this.ViewManager.LoadViewAsync(this.ViewManager.LastViewData);
}
public void OK()
{
if (IsModal) this.ModalParent.Close(ModalResult.OK());
else this.ViewManager.LoadViewAsync(this.ViewManager.LastViewData);
}
ControllerServiceFormBase
至此,在表单层次结构中,我们为泛型添加了一些复杂性。我们通过IControllerService
接口注入了Controller Service ,我们需要为其提供我们正在加载TRecord
和DbContext
以使用TContext
的RecordType
。类声明对泛型施加与IControllerService
相同的约束。其余的属性在代码块中描述。
// CEC.Blazor/Components/BaseForms/ControllerServiceFormBase.cs
public class ControllerServiceFormBase<TRecord, TContext> :
FormBase
where TRecord : class, IDbRecord<TRecord>, new()
where TContext : DbContext
{
/// Service with IDataRecordService Interface that corresponds to Type T
/// Normally set as the first line in the OnRender event.
public IControllerService<TRecord, TContext> Service { get; set; }
/// Property to control various UI Settings
/// Used as a cascadingparameter
[Parameter] public UIOptions UIOptions { get; set; } = new UIOptions();
/// The default alert used by all inherited components
/// Used for Editor Alerts, error messages, ....
[Parameter] public Alert AlertMessage { get; set; } = new Alert();
/// Property with generic error message for the Page Manager
protected virtual string RecordErrorMessage { get; set; } =
"The Application is loading the record.";
/// Boolean check if the Service exists
protected bool IsService { get => this.Service != null; }
}
RecordFormBase
所有记录显示表单都直接使用此表单。它介绍了记录管理。请注意,记录本身位于数据服务中。RecordFormBase
保留ID并调用Record Service来加载和重置记录。
// CEC.Blazor/Components/Base/RecordFormBase.cs
public class RecordFormBase<TRecord, TContext> :
ControllerServiceFormBase<TRecord, TContext>
where TRecord : class, IDbRecord<TRecord>, new()
where TContext : DbContext
{
/// This Page/Component Title
public virtual string PageTitle =>
(this.Service?.Record?.DisplayName ?? string.Empty).Trim();
/// Boolean Property that checks if a record exists
protected virtual bool IsRecord => this.Service?.IsRecord ?? false;
/// Used to determine if the page can display data
protected virtual bool IsError { get => !this.IsRecord; }
/// Used to determine if the page has display data i.e. it's not loading or in error
protected virtual bool IsLoaded => !(this.Loading) && !(this.IsError);
/// Property for the Record ID
[Parameter]
public int? ID
{
get => this._ID;
set => this._ID = (value is null) ? -1 : (int)value;
}
/// No Null Version of the ID
public int _ID { get; private set; }
protected async override Task OnRenderAsync(bool firstRender)
{
if (firstRender && this.IsService) await this.Service.ResetRecordAsync();
await this.LoadRecordAsync(firstRender);
await base.OnRenderAsync(firstRender);
}
/// Reloads the record if the ID has changed
protected virtual async Task LoadRecordAsync(bool firstload = false)
{
if (this.IsService)
{
// Set the Loading flag
this.Loading = true;
// call Render only if we are responding to an event.
// In the component loading cycle it will be called for us shortly
if (!firstload) await RenderAsync();
if (this.IsModal &&
this.ViewManager.ModalDialog.Options.Parameters.TryGetValue
("ID", out object modalid)) this.ID = (int)modalid > -1 ?
(int)modalid : this.ID;
// Get the current record - this will check if the id is
// different from the current record and only update if it's changed
await this.Service.GetRecordAsync(this._ID, false);
// Set the error message - it will only be displayed if we have an error
this.RecordErrorMessage =
$"The Application can't load the Record with ID: {this._ID}";
// Set the Loading flag
this.Loading = false;
// call Render only if we are responding to an event.
// In the component loading cycle it will be called for us shortly
if (!firstload) await RenderAsync();
}
}
}
EditRecordFormBase
所有记录编辑表单都直接使用此表单。它
- 根据记录状态管理表单状态。当状态为脏时,它将页面锁定在应用程序中,并通过浏览器导航挑战阻止浏览器导航。
- 保存记录。
// CEC.Blazor/Components/Base/EditRecordFormBase.cs
public class EditRecordFormBase<TRecord, TContext> :
RecordFormBase<TRecord, TContext>
where TRecord : class, IDbRecord<TRecord>, new()
where TContext : DbContext
{
/// Boolean Property exposing the Service Clean state
public bool IsClean => this.Service?.IsClean ?? true;
/// EditContext for the component
protected EditContext EditContext { get; set; }
/// Property to concatenate the Page Title
public override string PageTitle
{
get
{
if (this.IsNewRecord) return $"New
{this.Service?.RecordConfiguration?.RecordDescription ?? "Record"}";
else return $"{this.Service?.RecordConfiguration?.RecordDescription ??
"Record"} Editor";
}
}
/// Boolean Property to determine if the record is new or an edit
public bool IsNewRecord => this.Service?.RecordID == 0 ? true : false;
/// property used by the UIErrorHandler component
protected override bool IsError { get => !(this.IsRecord && this.EditContext != null); }
protected async override Task LoadRecordAsync(bool firstLoad = false)
{
await base.LoadRecordAsync(firstLoad);
//set up the Edit Context
this.EditContext = new EditContext(this.Service.Record);
}
protected async override Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
// Add the service listeners for the Record State
this.Service.OnDirty += this.OnRecordDirty;
this.Service.OnClean += this.OnRecordClean;
}
}
protected void OnRecordDirty(object sender, EventArgs e)
{
this.ViewManager.LockView();
this.AlertMessage.SetAlert("The Record isn't Saved", Bootstrap.ColourCode.warning);
InvokeAsync(this.Render);
}
protected void OnRecordClean(object sender, EventArgs e)
{
this.ViewManager.UnLockView();
this.AlertMessage.ClearAlert();
InvokeAsync(this.Render);
}
/// Event handler for the RecordFromControls FieldChanged Event
/// <param name="isdirty"></param>
protected virtual void RecordFieldChanged(bool isdirty)
{
if (this.EditContext != null) this.Service.SetDirtyState(isdirty);
}
/// Save Method called from the Button
protected virtual async Task<bool> Save()
{
var ok = false;
// Validate the EditContext
if (this.EditContext.Validate())
{
// Save the Record
ok = await this.Service.SaveRecordAsync();
if (ok)
{
// Set the EditContext State
this.EditContext.MarkAsUnmodified();
}
// Set the alert message to the return result
this.AlertMessage.SetAlert(this.Service.TaskResult);
// Trigger a component State update - buttons and alert need to be sorted
await RenderAsync();
}
else this.AlertMessage.SetAlert("A validation error occurred.
Check individual fields for the relevant error.", Bootstrap.ColourCode.danger);
return ok;
}
/// Save and Exit Method called from the Button
protected virtual async void SaveAndExit()
{
if (await this.Save()) this.ConfirmExit();
}
/// Confirm Exit Method called from the Button
protected virtual void TryExit()
{
// Check if we are free to exit ot need confirmation
if (this.IsClean) ConfirmExit();
}
/// Confirm Exit Method called from the Button
protected virtual void ConfirmExit()
{
// To escape a dirty component set IsClean manually and navigate.
this.Service.SetDirtyState(false);
// Sort the exit strategy
this.Exit();
}
protected override void Dispose(bool disposing)
{
this.Service.OnDirty -= this.OnRecordDirty;
this.Service.OnClean -= this.OnRecordClean;
base.Dispose(disposing);
}
}
实现编辑组件
所有表单和视图都在CEC.Weather
库中实现。因为这是一个库,所以没有_Imports.razor
因此所有组件使用的库必须在Razor文件中声明。
常见的ASPNetCore
设置有:
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Rendering;
@using Microsoft.AspNetCore.Components.Forms
View
View
是非常简单的。它
- 声明所有使用过的库。
- 将继承设置为
Component
——视图很简单。 - 实现
IView
因此可以将其加载为View
。 - 设置命名空间。
- 通过级联值得到
ViewManager
。 - 声明一个
ID
参数。 - 为
WeatherForecastEditorForm
添加Razor标记。
// CEC.Weather/Components/Views/WeatherForecastEditorView.razor
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Rendering;
@using Microsoft.AspNetCore.Components.Forms
@using CEC.Blazor.Components
@using CEC.Blazor.Components.BaseForms
@using CEC.Blazor.Components.UIControls
@using CEC.Weather.Data
@using CEC.Weather.Components
@using CEC.Blazor.Components.Base
@inherits Component
@implements IView
@namespace CEC.Weather.Components.Views
<WeatherForecastEditorForm ID="this.ID"></WeatherForecastEditorForm>
@code {
[CascadingParameter] public ViewManager ViewManager { get; set; }
[Parameter] public int ID { get; set; } = 0;
}
表单
代码文件相对简单,其中大多数细节都在Razor标记中。它
- 声明具有正确
Record
和DbContext
设置的类。 - 注入正确的Controller Service。
- 将控制器服务分配给
Service
。
// CEC.Weather/Components/Forms/WeatherForecastEditorForm.razor
public partial class WeatherForecastEditorForm : EditRecordFormBase<DbWeatherForecast,
WeatherForecastDbContext>
{
[Inject]
public WeatherForecastControllerService ControllerService { get; set; }
protected override Task OnRenderAsync(bool firstRender)
{
// Assign the correct controller service
if (firstRender) this.Service = this.ControllerService;
return base.OnRenderAsync(firstRender);
}
}
下面的“Razor标记”是完整文件的缩写版本。这将大量使用UIControl
,将在下一篇文章中详细介绍。有关详细信息,请参见注释。这里要注意的导入概念是Razor标记就是所有控件——看不到HTML。
// CEC.Weather/Components/Forms/WeatherForecastEditorForm.razor.cs
// UI Card is a Bootstrap Card
<UICard IsCollapsible="false">
<Header>
@this.PageTitle
</Header>
<Body>
// Cascades the Event Handler in the form for RecordChanged.
// Picked up by each FormControl and fired when a value changes in the FormControl
<CascadingValue Value="@this.RecordFieldChanged"
Name="OnRecordChange" TValue="Action<bool>">
// Error handler - only renders it's content when the record exists and is loaded
<UIErrorHandler IsError="@this.IsError"
IsLoading="this.Loading" ErrorMessage="@this.RecordErrorMessage">
<UIContainer>
// Standard Blazor EditForm control
<EditForm EditContext="this.EditContext">
// Fluent ValidationValidator for the form
<FluentValidationValidator DisableAssemblyScanning="@true" />
.....
// Example data value row with label and edit control
<UIFormRow>
<UILabelColumn Columns="4">
Record Date:
</UILabelColumn>
<UIColumn Columns="4">
// Note the Record Value bind to the record shadow copy
// to detect changes from the original stored value
<FormControlDate class="form-control"
@bind-Value="this.Service.Record.Date"
RecordValue="this.Service.ShadowRecord.Date">
</FormControlDate>
</UIColumn>
</UIFormRow>
..... // more form rows here
</EditForm>
</UIContainer>
</UIErrorHandler>
// Container for the buttons - not record dependant so outside the error handler
// to allow navigation if UIErrorHandler is in error.
<UIContainer>
<UIRow>
<UIColumn Columns="7">
<UIAlert Alert="this.AlertMessage"
SizeCode="Bootstrap.SizeCode.sm"></UIAlert>
</UIColumn>
<UIButtonColumn Columns="5">
<UIButton Show="(!this.IsClean) && this.IsLoaded"
ClickEvent="this.SaveAndExit"
ColourCode="Bootstrap.ColourCode.save">Save & Exit</UIButton>
<UIButton Show="(!this.IsClean) && this.IsLoaded"
ClickEvent="this.Save" ColourCode="Bootstrap.ColourCode.save">
Save</UIButton>
<UIButton Show="(!this.IsClean) &&
this.IsLoaded" ClickEvent="this.ConfirmExit"
ColourCode="Bootstrap.ColourCode.danger_exit">Exit Without Saving
</UIButton>
<UIButton Show="this.IsClean" ClickEvent="this.TryExit"
ColourCode="Bootstrap.ColourCode.nav">Exit</UIButton>
</UIButtonColumn>
</UIRow>
</UIContainer>
</CascadingValue>
</Body>
</UICard>
表单事件代码
组件事件代码
让我们更详细地了解正在发生的OnRenderAsync
事情。
OnInitializedAsync
从上到下实现OnRenderAsync
(在调用base方法之前运行本地代码)。它
- 将正确的数据服务分配给
Service
。 - 调用
ResetRecordAsync
以重置服务记录数据。 - 通过
LoadRecordAsync
加载记录。 - 获取用户信息。
// CEC.Weather/Components/Forms/WeatherEditorForm.razor.cs
protected override Task OnRenderAsync(bool firstRender)
{
// Assign the correct controller service
if (firstRender) this.Service = this.ControllerService;
return base.OnRenderAsync(firstRender);
}
// CEC.Blazor/Components/BaseForms/RecordFormBase.cs
protected async override Task OnRenderAsync(bool firstRender)
{
if (firstRender && this.IsService) await this.Service.ResetRecordAsync();
await this.LoadRecordAsync(firstRender);
await base.OnRenderAsync(firstRender);
}
// CEC.Blazor/Components/BaseForms/ApplicationComponentBase.cs
protected async override Task OnRenderAsync(bool firstRender)
{
if (firstRender) {
await GetUserAsync();
}
await base.OnRenderAsync(firstRender);
}
LoadRecordAsync
记录加载代码已分解,因此可以在组件事件驱动的方法之外使用。它是自下而上实现的(在任何本地代码之前都会调用base方法)。
主要的记录加载功能是根据ID获取和加载记录的RecordFormBase
。EditFormBase
添加了额外的编辑功能——它为记录创建了编辑上下文。
// CEC.Blazor/Components/BaseForms/RecordComponentBase.cs
protected virtual async Task LoadRecordAsync(bool firstload = false)
{
if (this.IsService)
{
// Set the Loading flag
this.Loading = true;
// call Render only if we are not responding to first load
if (!firstload) await RenderAsync();
if (this.IsModal && this.ViewManager.ModalDialog.Options.Parameters.TryGetValue
("ID", out object modalid)) this.ID = (int)modalid > -1 ? (int)modalid : this.ID;
// Get the current record - this will check if the id is different from
// the current record and only update if it's changed
await this.Service.GetRecordAsync(this._ID, false);
// Set the error message - it will only be displayed if we have an error
this.RecordErrorMessage =
$"The Application can't load the Record with ID: {this._ID}";
// Set the Loading flag
this.Loading = false;
// call Render only if we are not responding to first load
if (!firstload) await RenderAsync();
}
}
// CEC.Blazor/Components/BaseForms/EditComponentBase.cs
protected async override Task LoadRecordAsync(bool firstLoad = false)
{
await base.LoadRecordAsync(firstLoad);
//set up the Edit Context
this.EditContext = new EditContext(this.Service.Record);
}
OnAfterRenderAsync
OnAfterRenderAsync
是自下而上实现的(在执行任何本地代码之前调用了base)。它将记录dirty事件分配给本地表单事件。
// CEC.Blazor/Components/BaseForms/EditFormBase.cs
protected async override Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
this.Service.OnDirty += this.OnRecordDirty;
this.Service.OnClean += this.OnRecordClean;
}
}
事件处理程序
在组件加载事件中连接了一个事件处理程序。
// CEC.Blazor/Components/BaseForms/EditComponentBase.cs
// Event handler for the Record Form Controls FieldChanged Event
// wired to each control through a cascaded parameter
protected virtual void RecordFieldChanged(bool isdirty)
{
if (this.EditContext != null) this.Service.SetDirtyState(isdirty);
}
Action按钮事件
有各种动作连接到按钮。重要的是保存。
// CEC.Blazor/Components/BaseForms/EditRecordComponentBase.cs
/// Save Method called from the Button
protected virtual async Task<bool> Save()
{
var ok = false;
// Validate the EditContext
if (this.EditContext.Validate())
{
// Save the Record
ok = await this.Service.SaveRecordAsync();
if (ok)
{
// Set the EditContext State
this.EditContext.MarkAsUnmodified();
}
// Set the alert message to the return result
this.AlertMessage.SetAlert(this.Service.TaskResult);
// Trigger a component State update - buttons and alert need to be sorted
await RenderAsync();
}
else this.AlertMessage.SetAlert("A validation error occurred.
Check individual fields for the relevant error.", Bootstrap.ColourCode.danger);
return ok;
}
实现视图页面
View
路由视图非常简单。它包含路由和要加载的组件。
@using CEC.Blazor.Components
@using CEC.Weather.Components
@using CEC.Blazor.Components.Base
@namespace CEC.Weather.Components.Views
@implements IView
@inherits Component
<WeatherForecastViewerForm ID="this.ID"></WeatherForecastViewerForm>
@code {
[CascadingParameter] public ViewManager ViewManager { get; set; }
[Parameter] public int ID { get; set; } = 0;
}
表单
代码文件相对简单,其中大多数细节都在Razor标记中。
// CEC.Weather/Components/Forms/WeatherViewerForm.razor
public partial class WeatherForecastViewerForm :
RecordFormBase<DbWeatherForecast, WeatherForecastDbContext>
{
[Inject]
private WeatherForecastControllerService ControllerService { get; set; }
public override string PageTitle => $"Weather Forecast Viewer
{this.Service?.Record?.Date.AsShortDate() ?? string.Empty}".Trim();
protected override Task OnRenderAsync(bool firstRender)
{
if (firstRender)
{
this.Service = this.ControllerService;
}
return base.OnRenderAsync(firstRender);
}
protected async void NextRecord(int increment)
{
var rec = (this._ID + increment) == 0 ? 1 : this._ID + increment;
rec = rec > this.Service.BaseRecordCount ? this.Service.BaseRecordCount : rec;
this.ID = rec;
await this.ResetAsync();
}
}
这将通过DI获取并将其ControllerService
分配给IContollerService Service
属性。
下面的“Razor标记”是完整文件的缩写。这将广泛使用UIControls
,将在以后的文章中详细介绍。有关详细信息,请参见注释。
// CEC.Weather/Components/Forms/WeatherViewerForm.razor.cs
// UI Card is a Bootstrap Card
<UICard IsCollapsible="false">
<Header>
@this.PageTitle
</Header>
<Body>
// Error handler - only renders it's content when the record exists and is loaded
<UIErrorHandler IsError="@this.IsError"
IsLoading="this.Loading" ErrorMessage="@this.RecordErrorMessage">
<UIContainer>
.....
// Example data value row with label and edit control
<UIRow>
<UILabelColumn Columns="2">
Date
</UILabelColumn>
<UIColumn Columns="2">
<FormControlPlainText
Value="@this.Service.Record.Date.AsShortDate()">
</FormControlPlainText>
</UIColumn>
<UILabelColumn Columns="2">
ID
</UILabelColumn>
<UIColumn Columns="2">
<FormControlPlainText Value="@this.Service.Record.ID.ToString()">
</FormControlPlainText>
</UIColumn>
<UILabelColumn Columns="2">
Frost
</UILabelColumn>
<UIColumn Columns="2">
<FormControlPlainText
Value="@this.Service.Record.Frost.AsYesNo()">
</FormControlPlainText>
</UIColumn>
</UIRow>
..... // more form rows here
</UIContainer>
</UIErrorHandler>
// Container for the buttons - not record dependant so outside the error handler
// to allow navigation if UIErrorHandler is in error.
<UIContainer>
<UIRow>
<UIColumn Columns="6">
<UIButton Show="this.IsLoaded" ColourCode="Bootstrap.ColourCode.dark"
ClickEvent="(e => this.NextRecord(-1))">
Previous
</UIButton>
<UIButton Show="this.IsLoaded" ColourCode="Bootstrap.ColourCode.dark"
ClickEvent="(e => this.NextRecord(1))">
Next
</UIButton>
</UIColumn>
<UIButtonColumn Columns="6">
<UIButton Show="true" ColourCode="Bootstrap.ColourCode.nav"
ClickEvent="(e => this.Exit())">
Exit
</UIButton>
</UIButtonColumn>
</UIRow>
</UIContainer>
</Body>
</UICard>
总结
总结了这篇文章。我们已经详细研究了Editor代码以了解其工作原理,然后快速查看了Viewer代码。我们将在另一篇文章中更详细地介绍这些List
组件。
需要注意的一些关键点:
- Blazor服务器和Blazor WASM代码相同——在公共库中。
- 几乎所有功能都在库组件中实现。大多数应用程序代码都是单个记录字段的Razor标记。
- Razor文件包含控件,而不是HTML。
- 通过使用异步功能。