在Blazor中构建数据库应用程序——第5部分——查看组件——UI中的CRUD列表操作

目录

介绍

储存库和数据库

列表功能

基本表单

分页

初始表单加载

表单事件

页面控件

WeatherForecastListForm

WeatherForcastListModalView

总结


介绍

这是该系列文章的第五篇,探讨了如何在Blazor中构建和构建真正的数据库应用程序。

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

本文详细介绍了构建可重用的列表表示层组件并将其部署在ServerWASM项目中的情况。

储存库和数据库

CEC.Blazor GitHub存储库

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

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

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

列表功能

列表组件比其他CRUD组件面临的挑战要多得多。专业级别列表控件中预期的功能包括:

  • 分页以处理大型数据集
  • 列格式化以控制列宽和数据溢出
  • 在各个列上排序
  • 筛选

基本表单

ListFormBase是所有列表的基本表单。它继承自ControllerServiceFormBase

文章中并没有显示所有代码——有些类太大了,我只显示最相关的部分。可以在Github站点上查看所有源文件,并且在本文的适当位置提供了对特定代码文件的引用或链接。代码注释中有关于代码段的详细信息。

分页

分页是通过IControllerPagingService接口实现的。BaseControllerService实现此接口。由于太大,此处未详细显示。许多功能非常明显——可以跟踪您所在页面的属性,具有多少页面和块、页面大小等的属性——因此我们将跳到更有趣的部分。

初始表单加载

让我们从加载列表表单开始,然后看一下OnRenderAsync

// CEC.Weather/Components/Forms/WeatherForecastListForm.razor.cs
protected override Task OnRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        // Sets the specific service
        this.Service = this.ControllerService;
        // Sets the max column
        this.UIOptions.MaxColumn = 3;
    }
    return base.OnRenderAsync(firstRender);
}
// CEC.Blazor/Components/BaseForms/ListFormBase.cs
protected async override Task OnRenderAsync(bool firstRender)
{
    // set the page to 1
    var page = 1;
    if (this.IsService)
    {
        if (firstRender)
        {
            // Reset the Service if this is the first load
            await this.Service.Reset();
            this.ListTitle = string.IsNullOrEmpty(this.ListTitle) ? 
            $"List of {this.Service.RecordConfiguration.RecordListDecription}" : 
                       this.ListTitle;
        }
        // Load the filters for the recordset
        this.LoadFilter();
        // Check if we have a saved Page No in the ViewData
        if (this.IsViewManager && 
           !this.ViewManager.ViewData.GetFieldAsInt("Page", out page)) page = 1;
        // Load the paged recordset
        await this.Service.LoadPagingAsync();
        // go to the correct page
        await this.Paging.GoToPageAsync(page);
        this.Loading = false;
    }
    await base.OnRenderAsync(firstRender);
}
// CEC.Blazor/Components/BaseForms/FormBase.cs
protected async override Task OnRenderAsync(bool firstRender)
{
    if (firstRender) {
        await GetUserAsync();
    }
    await base.OnRenderAsync(firstRender);
}
// base LoadFilter
protected virtual void LoadFilter()
{
    // Set OnlyLoadIfFilter if the Parameter value is set
    if (IsService) this.Service.FilterList.OnlyLoadIfFilters = this.OnlyLoadIfFilter;
}

我们有兴趣ListFormBase调用LoadPagingAsync()

// CEC.Blazor/Components/BaseForms/ListComponentBase.cs

/// Method to load up the Paged Data to display
/// loads the delegate with the default service GetDataPage method and loads the first page
/// Can be overridden for more complex situations
public async virtual Task LoadPagingAsync()
{
    // set the record to null to force a reload of the records
    this.Records = null;
    // if requested adds a default service function to the delegate
    this.PageLoaderAsync = new IControllerPagingService<TRecord>.PageLoaderDelegateAsync
                           (this.GetDataPageWithSortingAsync);
    // loads the paging object
    await this.LoadAsync();
    // Trigger event so any listeners get notified
    this.TriggerListChangedEvent(this);
}

IControllerPagingService定义一个返回TRecordsListPageLoaderDelegateAsync委托和一个委托属性PageLoaderAsync

// CEC.Blazor/Services/Interfaces/IControllerPagingService.cs

// Delegate that returns a generic Record List
public delegate Task<List<TRecord>> PageLoaderDelegateAsync();

// Holds the reference to the PageLoader Method to use
public PageLoaderDelegateAsync PageLoaderAsync { get; set; }

默认情况下,LoadPagingAsync将方法GetDataPageWithSortingAsync加载为PageLoaderAsync,然后调用LoadAsync

LoadAsync重置各种属性(我们在OnRenderAsync中通过重置服务来完成此操作,但是LoadAsync在需要重置记录列表的其他地方调用了它-而不是整个服务),然后调用了PageLoaderAsync委托。

// CEC.Blazor/Services/BaseControllerService.cs
public async Task<bool> LoadAsync()
{
    // Reset the page to 1
    this.CurrentPage = 1;
    // Check if we have a sort column, if not set to the default column
    if (!string.IsNullOrEmpty(this.DefaultSortColumn)) 
         this.SortColumn = this.DefaultSortColumn;
    // Set the sort direction to the default
    this.SortingDirection = DefaultSortingDirection;
    // Check if we have a method loaded in the PageLoaderAsync delegate and if so run it
    if (this.PageLoaderAsync != null) this.PagedRecords = await this.PageLoaderAsync();
    // Set the block back to the start
    this.ChangeBlock(0);
    //  Force a UI update as everything has changed
    this.PageHasChanged?.Invoke(this, this.CurrentPage);
    return true;
}

GetDataPageWithSortingAsync如下所示。有关详细信息,请参见注释。如有必要,GetFilteredListAsync总是调用来刷新列表recordset

// CEC.Blazor/Services/BaseControllerService.cs
public async virtual Task<List<TRecord>> GetDataPageWithSortingAsync()
{
    // Get the filtered list - will only get a new list if the Records property 
    // has been set to null elsewhere
    await this.GetFilteredListAsync();
    // Reset the start record if we are outside 
    // the range of the record set - a belt and braces check as this shouldn't happen!
    if (this.PageStartRecord > this.Records.Count) this.CurrentPage = 1;
    // Check if we have to apply sorting, in not get the page we want
    if (string.IsNullOrEmpty(this.SortColumn)) 
    return this.Records.Skip(this.PageStartRecord).Take(this._PageSize).ToList();
    else
    {
        //  If we do order the record set and then get the page we want
        if (this.SortingDirection == SortDirection.Ascending)
        {
            return this.Records.OrderBy(x => x.GetType().GetProperty(this.SortColumn).
            GetValue(x, null)).Skip(this.PageStartRecord).Take(this._PageSize).ToList();
        }
        else
        {
            return this.Records.OrderByDescending
                   (x => x.GetType().GetProperty(this.SortColumn).
            GetValue(x, null)).Skip(this.PageStartRecord).Take(this._PageSize).ToList();
        }
    }
}

GetFilteredListAsync得到一个过滤列表recordset。在应用过滤的表单组件中(例如从过滤器控件中)覆盖了它。默认实现获取整个recordset。它使用IsRecords来检查它是否需要重新加载recordset。如果Recordsnull,则仅重新加载。

// CEC.Blazor/Services/BaseControllerService.cs
public async virtual Task<bool> GetFilteredListAsync()
{
    // Check if the record set is null. and only refresh the record set if it's null
    if (!this.IsRecords)
    {
        //gets the filtered record list
        this.Records = await this.Service.GetFilteredRecordListAsync(FilterList);
        return true;
    }
    return false;
}

总结一下:

  1. 在窗体加载Service时(记录类型的特定数据服务)将重置。
  2. Service上的GetDataPageWithSortingAsync()被加载到Service委托。
  3. Service中的Records设置为null时调用Delegate
  4. GetFilteredListAsync()加载Records
  5. GetDataPageWithSortingAsync() 加载第一页。
  6. IsLoading设置为false,以便UIErrorHandler UI控件显示页面。
  7. Form后刷新OnParametersSetAsync自动所以没有手动调用StateHasChanged是必需的。OnInitializedAsync是的一部分,OnParametersSetAsync只有完成OnInitializedAsync后才能完成。

表单事件

PagingControl通过IPagingControlService接口直接与表单Service进行交互,并将单击按钮链接到IPagingControlService方法:

  1. ChangeBlockAsync(int direction,bool supresspageupdate)
  2. MoveOnePageAsync(int direction)
  3. GoToPageAsync(int pageno)
// CEC.Blazor/Services/BaseControllerService.cs

/// Moves forward or backwards one block
/// direction 1 for forwards
/// direction -1 for backwards
/// suppresspageupdate 
///  - set to true (default) when user changes page and the block changes with the page
///  - set to false when user changes block rather than changing page 
/// and the page needs to be updated to the first page of the block
public async Task ChangeBlockAsync(int direction, bool suppresspageupdate = true)
{
    if (direction == 1 && this.EndPage < this.TotalPages)
    {
        this.StartPage = this.EndPage + 1;
        if (this.EndPage + this.PagingBlockSize < this.TotalPages) 
            this.EndPage = this.StartPage + this.PagingBlockSize - 1;
        else this.EndPage = this.TotalPages;
        if (!suppresspageupdate) this.CurrentPage = this.StartPage;
    }
    else if (direction == -1 && this.StartPage > 1)
    {
        this.EndPage = this.StartPage - 1;
        this.StartPage = this.StartPage - this.PagingBlockSize;
        if (!suppresspageupdate) this.CurrentPage = this.StartPage;
    }
    else if (direction == 0 && this.CurrentPage == 1)
    {
        this.StartPage = 1;
        if (this.EndPage + this.PagingBlockSize < this.TotalPages) 
            this.EndPage = this.StartPage + this.PagingBlockSize - 1;
        else this.EndPage = this.TotalPages;
    }
    if (!suppresspageupdate) await this.PaginateAsync();
}
/// Moves forward or backwards one page
/// direction 1 for forwards
/// direction -1 for backwards
public async Task MoveOnePageAsync(int direction)
{
    if (direction == 1)
    {
        if (this.CurrentPage < this.TotalPages)
        {
            if (this.CurrentPage == this.EndPage) await ChangeBlockAsync(1);
            this.CurrentPage += 1;
        }
    }
    else if (direction == -1)
    {
        if (this.CurrentPage > 1)
        {
            if (this.CurrentPage == this.StartPage) await ChangeBlockAsync(-1);
            this.CurrentPage -= 1;
        }
    }
    await this.PaginateAsync();
}
/// Moves to the specified page
public Async Task GoToPageAsync(int pageno)
{
    this.CurrentPage = pageno;
    await this.PaginateAsync();
}

上面所有方法都设置了IPagingControlService属性,然后调用PaginateAsync(),该调用将调用PageLoaderAsync委托并强制UI更新。

// CEC.Blazor/Services/BaseControllerService.cs

/// Method to trigger the page Changed Event
public async Task PaginateAsync()
{
    // Check if we have a method loaded in the PageLoaderAsync delegate and if so run it
    if (this.PageLoaderAsync != null) this.PagedRecords = await this.PageLoaderAsync();
    //  Force a UI update as something has changed
    this.PageHasChanged?.Invoke(this, this.CurrentPage);
}

页面控件

PageControl代码如下所示,并带有注释。

// CEC.Blazor/Components/FormControls/PagingControl.razor

@if (this.IsPagination)
{
    <div class="pagination ml-2 flex-nowrap">
        <nav aria-label="Page navigation">
            <ul class="pagination mb-1">
                @if (this.DisplayType != PagingDisplayType.Narrow)
                {
                    @if (this.DisplayType == PagingDisplayType.FullwithoutPageSize)
                    {
                        <li class="page-item"><button class="page-link" 
                        @onclick="(e => this.Paging.ChangeBlockAsync
                        (-1, false))">1«</button></li>
                    }
                    else
                    {
                        <li class="page-item"><button class="page-link" 
                        @onclick="(e => this.Paging.ChangeBlockAsync
                        (-1, false))">«</button></li>
                    }
                    <li class="page-item"><button class="page-link" 
                    @onclick="(e => this.Paging.MoveOnePageAsync(-1))">Previous
                    </button></li>
                    @for (int i = this.Paging.StartPage; i <= this.Paging.EndPage; i++)
                    {
                        var currentpage = i;
                        <li class="page-item @(currentpage == this.Paging.CurrentPage ? 
                        "active" : "")"><button class="page-link" 
                        @onclick="(e => this.Paging.GoToPageAsync(currentpage))">@currentpage
                        </button></li>
                    }
                    <li class="page-item"><button class="page-link" 
                    @onclick="(e => this.Paging.MoveOnePageAsync(1))">Next
                    </button></li>
                    @if (this.DisplayType == PagingDisplayType.FullwithoutPageSize)
                    {
                        <li class="page-item"><button class="page-link" 
                        @onclick="(e => this.Paging.ChangeBlockAsync(1, false))">»
                        @this.Paging.TotalPages
                        </button></li>
                    }
                    else
                    {
                        <li class="page-item"><button class="page-link" 
                        @onclick="(e => this.Paging.ChangeBlockAsync(1, false))">»
                        </button></li>
                    }
                }
                else
                {
                    <li class="page-item"><button class="page-link" 
                    @onclick="(e => this.Paging.MoveOnePageAsync(-1))">1«
                    </button></li>
                    @for (int i = this.Paging.StartPage; i <= this.Paging.EndPage; i++)
                    {
                        var currentpage = i;
                        <li class="page-item @(currentpage == this.Paging.CurrentPage ? 
                        "active" : "")"><button class="page-link" 
                        @onclick="(e => 
                        this.Paging.GoToPageAsync(currentpage))">@currentpage
                        </button></li>
                    }
                    <li class="page-item"><button class="page-link" 
                    @onclick="(e => this.Paging.MoveOnePageAsync(1))">»
                    @this.Paging.TotalPages
                    </button></li>
                }
            </ul>
        </nav>
        @if (this.DisplayType == PagingDisplayType.Full)
        {
            <span class="pagebutton btn btn-link btn-sm disabled mr-1">Page 
            @this.Paging.CurrentPage of @this.Paging.TotalPages</span>
        }
    </div>
}
// CEC.Blazor/Components/FormControls/PagingControl.razor.cs

public partial class PagingControl<TRecord> : 
       ComponentBase where TRecord : IDbRecord<TRecord>, new()
{
    [CascadingParameter] public IControllerPagingService<TRecord> _Paging { get; set; }

    [Parameter] public IControllerPagingService<TRecord> Paging { get; set; }

    [Parameter] public PagingDisplayType DisplayType { get; set; } = PagingDisplayType.Full;

    [Parameter] public int BlockSize { get; set; } = 0;

    private bool IsPaging => this.Paging != null;

    private bool IsPagination => this.Paging != null && this.Paging.IsPagination;

    protected override Task OnRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            // Check if we have a cascaded IControllerPagingService if so use it
            this.Paging = this._Paging ?? this.Paging;
        }
        if (this.IsPaging)
        {
            this.Paging.PageHasChanged += this.UpdateUI;
            if (this.DisplayType == PagingDisplayType.Narrow) Paging.PagingBlockSize = 4;
            if (BlockSize > 0) Paging.PagingBlockSize = this.BlockSize;
        }
        return base.OnRenderAsync(firstRender);
    }

    protected async void UpdateUI(object sender, int recordno) => await RenderAsync();

    private string IsCurrent(int i) => 
            i == this.Paging.CurrentPage ? "active" : string.Empty;
}

WeatherForecastListForm

WeatherForecastListForm的代码很容易解释。OnViewOnEdit路由到查看器或编辑器,或者如果UIOptions指定了使用模态,则打开对话框。

// CEC.Weather/Components/Forms/WeatherForecastListForm.razor.cs

public partial class WeatherForecastListForm : 
       ListFormBase<DbWeatherForecast, WeatherForecastDbContext>
{
    /// The Injected Controller service for this record
    [Inject] protected WeatherForecastControllerService ControllerService { get; set; }

    protected override Task OnRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            // Sets the specific service
            this.Service = this.ControllerService;
            // Sets the max column
            this.UIOptions.MaxColumn = 3;
        }
        return base.OnRenderAsync(firstRender);
    }

    /// Method called when the user clicks on a row in the viewer.
    protected void OnView(int id)
    {
        if (this.UIOptions.UseModalViewer && this.ViewManager.ModalDialog != null) 
        this.OnModalAsync<WeatherForecastViewerForm>(id);
        else this.OnViewAsync<WeatherForecastViewerView>(id);
    }

    /// Method called when the user clicks on a row Edit button.
    protected void OnEdit(int id)
    {
        if (this.UIOptions.UseModalViewer && this.ViewManager.ModalDialog != null) 
        this.OnModalAsync<WeatherForecastEditorForm>(id);
        else this.OnViewAsync<WeatherForecastEditorView>(id);
    }
}

下面的“Razor标记是完整文件的缩写。这充分利用了上一篇文章中介绍的UIControls内容。有关详细信息,请参见注释。

// CEC.Weather/Components/Forms/WeatherForecastListForm.razor

// Wrapper that cascades the values including event handlers
<UIWrapper UIOptions="@this.UIOptions" 
RecordConfiguration="@this.Service.RecordConfiguration" 
OnView="@OnView" OnEdit="@OnEdit">

    // UI CardGrid is a Bootstrap Card
    <UICardGrid TRecord="DbWeatherForecast" 
    IsCollapsible="true" Paging="this.Paging" IsLoading="this.Loading">

        <Title>
            @this.ListTitle
        </Title>

        // Header Section of UICardGrid
        <TableHeader>
            // Header Grid columns
            <UIGridTableHeaderColumn TRecord="DbWeatherForecast" 
            Column="1" FieldName="WeatherForecastID">ID</UIGridTableHeaderColumn>
            <UIGridTableHeaderColumn TRecord="DbWeatherForecast" 
            Column="2" FieldName="Date">Date</UIGridTableHeaderColumn>
            .....
            <UIGridTableHeaderColumn TRecord="DbWeatherForecast" 
            Column="7"></UIGridTableHeaderColumn>
        </TableHeader>

        // Row Template Section of UICardGrid
        <RowTemplate>
            // Cascaded ID for the specific Row
            <CascadingValue Name="RecordID" Value="@context.WeatherForecastID">
                <UIGridTableColumn TRecord="DbWeatherForecast" 
                Column="1">@context.WeatherForecastID</UIGridTableColumn>
                <UIGridTableColumn TRecord="DbWeatherForecast" 
                Column="2">@context.Date.ToShortDateString()</UIGridTableColumn>
                .....
                <UIGridTableEditColumn TRecord="DbWeatherForecast"></UIGridTableEditColumn>
            </CascadingValue>

        </RowTemplate>
        // Navigation Section of UUCardGrid
        <Navigation>

            <UIListButtonRow>
                // Paging part of UIListButtonRow
                <Paging>
                    <PagingControl TRecord="DbWeatherForecast" 
                    Paging="this.Paging"></PagingControl>
                </Paging>
            </UIListButtonRow>

        </Navigation>
    </UICardGrid>
</UIWrapper>

Razor组件使用一组UIControl用于列表构建的。UICardGrid构建Bootstrap卡和表框架。您可以在Github存储库中浏览组件代码。

WeatherForcastListModalView

这很简单。WeatherForecastListFormUIOptions对象的Razor标记。

// CEC.Weather/Components/Views/WeatherForecastListModalView.razor

@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

@namespace CEC.Weather.Components.Views

@inherits Component
@implements IView

<WeatherForecastListForm UIOptions="this.UIOptions"></WeatherForecastListForm>

@code {

    [CascadingParameter]
    public ViewManager ViewManager { get; set; }

    public UIOptions UIOptions => new UIOptions()
    {
        ListNavigationToViewer = true,
        ShowButtons = true,
        ShowAdd = true,
        ShowEdit = true,
        UseModalEditor = true,
        UseModalViewer = true
    };
}

总结

总结了这篇文章。需要注意的一些关键点:

  1. Blazor服务器和Blazor WASM之间的代码没有差异。
  2. 几乎所有功能都在库组件中实现。大多数应用程序代码都是单个记录字段的Razor标记。
  3. 整个组件和CRUD数据访问都使用了异步功能。
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值