您当前的位置: 首页 >  ui

寒冰屋

暂无认证

  • 0浏览

    0关注

    2286博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

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

寒冰屋 发布时间:2020-12-12 20:23:54 ,浏览量:0

目录

介绍

示例项目和代码

基本表单

表单库

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表示层组件,尤其是“编辑”和“查看”功能——并将其用于Server和WASM项目。

示例项目和代码

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 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 : 
        FormBase 
        where TRecord : class, IDbRecord, 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 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 :
        ControllerServiceFormBase
        where TRecord : class, IDbRecord, 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 :
    RecordFormBase
    where TRecord : class, IDbRecord, 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
    /// 
    protected virtual void RecordFieldChanged(bool isdirty)
    {
        if (this.EditContext != null) this.Service.SetDirtyState(isdirty);
    }

    /// Save Method called from the Button
    protected virtual async Task 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



@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
{
    [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

    
        @this.PageTitle
    
    
        // Cascades the Event Handler in the form for RecordChanged. 
        // Picked up by each FormControl and fired when a value changes in the FormControl
        
            // Error handler - only renders it's content when the record exists and is loaded
            
                
                    // Standard Blazor EditForm control
                    
                        // Fluent ValidationValidator for the form
                        
                        .....
                        // Example data value row with label and edit control
                        
                            
                                Record Date:
                            
                            
                                // Note the Record Value bind to the record shadow copy 
                                // to detect changes from the original stored value
                                
                                
                            
                        
                        ..... // more form rows here
                    
                
            
            // Container for the buttons - not record dependant so outside the error handler 
            // to allow navigation if UIErrorHandler is in error.
            
                
                    
                        
                    
                    
                        Save & Exit
                        
                        Save
                        Exit Without Saving
                        
                        Exit
                    
                
            
        
    
表单事件代码
组件事件代码

让我们更详细地了解正在发生的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 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



@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
{
    [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

    
        @this.PageTitle
    
    
        // Error handler - only renders it's content when the record exists and is loaded
        
            
                    .....
                    // Example data value row with label and edit control
                    
                        
                            Date
                        

                        
                            
                            
                        

                        
                            ID
                        

                        
                            
                            
                        

                        
                            Frost
                        

                        
                            
                            
                        
                    
                    ..... // more form rows here
            
        
        // Container for the buttons - not record dependant so outside the error handler 
        // to allow navigation if UIErrorHandler is in error.
        
            
                
                    
                        Previous
                    
                    
                        Next
                    
                
                
                    
                        Exit
                    
                
            
        
    
总结

总结了这篇文章。我们已经详细研究了Editor代码以了解其工作原理,然后快速查看了Viewer代码。我们将在另一篇文章中更详细地介绍这些List组件。

需要注意的一些关键点:

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

微信扫码登录

0.2029s