在Blazor中构建数据库应用程序——第3部分——UI中的CRUD编辑和查看操作

目录

介绍

示例项目和代码

基本表单

表单库

ControllerServiceFormBase

RecordFormBase

EditRecordFormBase

实现编辑组件

View

表单

表单事件代码

组件事件代码

OnInitializedAsync

LoadRecordAsync

OnAfterRenderAsync

事件处理程序

Action按钮事件

实现视图页面

View

表单

总结


介绍

这是有关如何在Blazor中构建和构造真正的数据库应用程序的系列文章中的第三篇。

  1. 项目结构与框架
  2. 服务——构建CRUD数据层
  3. View组件——UI中的CRUD编辑和查看操作
  4. UI组件——构建HTML / CSS控件
  5. View组件-UI中的CRUD列表操作
  6. 逐步详细介绍如何向应用程序添加气象站和气象站数据

本文详细介绍了如何构建可重用的CRUD表示层组件,尤其是编辑查看功能——并将其用于ServerWASM项目。

示例项目和代码

CEC.Blazor GitHub存储库

存储库中有一个SQL脚本在/SQL中,用于构建数据库。

您可以在此处查看运行的项目的服务器版本。

你可以看到该项目的WASM版本运行在这里

基本表单

所有CRUD UI组件都继承自Component。文章中并没有显示所有代码:有些类太大了,无法显示所有内容。可以在Github站点上查看所有源文件,并且在本文的适当位置提供了对特定代码文件的引用或链接。许多信息详细信息在代码部分的注释中。

表单库

所有表格都继承自FormBaseFormBase提供以下功能:

  1. OwningComponentBase复制源代码以实现作用域服务管理
  2. 如果启用了身份验证,则获取用户
  3. 在模式或非模式状态下管理表单关闭
  4. 实现IFormIDisposable接口

作用域管理代码如下所示。您可以在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 ,我们需要为其提供我们正在加载TRecordDbContext以使用TContextRecordType。类声明对泛型施加与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

所有记录编辑表单都直接使用此表单。

  1. 根据记录状态管理表单状态。当状态为脏时,它将页面锁定在应用程序中,并通过浏览器导航挑战阻止浏览器导航。
  2. 保存记录。
// 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是非常简单的。它

  1. 声明所有使用过的库。
  2. 将继承设置为Component——视图很简单。
  3. 实现IView因此可以将其加载为View
  4. 设置命名空间。
  5. 通过级联值得到ViewManager
  6. 声明一个ID参数。
  7. 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标记中。

  1. 声明具有正确RecordDbContext设置的类。
  2. 注入正确的Controller Service
  3. 将控制器服务分配给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方法之前运行本地代码)。它

  1. 将正确的数据服务分配给Service
  2. 调用ResetRecordAsync以重置服务记录数据。
  3. 通过LoadRecordAsync加载记录。
  4. 获取用户信息。
// 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获取和加载记录的RecordFormBaseEditFormBase添加了额外的编辑功能——它为记录创建了编辑上下文。

// 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组件。

需要注意的一些关键点:

  1. Blazor服务器和Blazor WASM代码相同——在公共库中。
  2. 几乎所有功能都在库组件中实现。大多数应用程序代码都是单个记录字段的Razor标记。
  3. Razor文件包含控件,而不是HTML
  4. 通过使用异步功能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值