目录
介绍
存储库和数据库
列表功能
基本表单
分页和排序
分页器
分页器数据
分页器控件
排序控件
UIDataTableHeaderColumn
天气预报列表表单
视图
WeatherForecastComponent
RouteViews(又名页面)
总结
介绍本文是构建Blazor数据库应用程序系列中的第五篇。到目前为止的文章是:
- 项目结构和框架——一些介绍。
- 服务——构建CRUD数据层。
- 查看组件——UI中的CRUD编辑和查看操作。
- UI组件——构建 HTML/CSS控件。
- 查看组件——UI中的CRUD列表操作。
本文详细介绍了构建可重用的List UI组件并将它们部署在Server和WASM项目中。
存储库和数据库文章的存储库已移至Blazor.Database存储库。较旧的存储库现已过时,将很快被删除。
存储库中的/SQL中有一个用于构建数据库的SQL脚本。
您可以在同一站点上看到在此处运行的项目的Server和WASM版本。
列表功能列表组件比其他CRUD组件面临更多挑战。生产级清单控制中预期的功能包括:
* 分页——处理大型数据集
* 列格式——控制列宽和数据溢出
* 排序——在列上
* 过滤——此处未涵盖。
基本表单ListFormBase是所有列表的基本抽象形式。它继承自ComponentBase,并包含所有样板代码。 TRecord是它操作的数据类。该表格使用代码如下所示。
public abstract class ListFormBase : ComponentBase, IDisposable where TRecord : class, IDbRecord, new()
{
/// Callbacks for Edit/View/New/Exit Actions
[Parameter] public EventCallback EditRecord { get; set; }
[Parameter] public EventCallback ViewRecord { get; set; }
[Parameter] public EventCallback NewRecord { get; set; }
[Parameter] public EventCallback ExitAction { get; set; }
/// Controller Data Service
[Inject] protected IFactoryControllerService Service { get; set; }
[Inject] protected NavigationManager NavManager { get; set; }
/// Booleans for Service and Recordlist state
protected bool IsLoaded => this.Service?.HasRecords ?? false;
protected bool HasService => this.Service != null;
protected override async Task OnInitializedAsync()
{
if (this.HasService)
{
await this.Service.GetRecordsAsync();
this.Service.ListHasChanged += OnListChanged;
}
}
/// Call StatehasChanged if list changed
protected void OnListChanged(object sender, EventArgs e)
=> this.InvokeAsync(this.StateHasChanged);
/// Event handlers to call EventCallbacks
protected virtual void Edit(int id)
=> this.EditRecord.InvokeAsync(id);
protected virtual void View(int id)
=> this.ViewRecord.InvokeAsync(id);
protected virtual void New()
=> this.NewRecord.InvokeAsync();
protected virtual void Exit()
{
if (ExitAction.HasDelegate)
ExitAction.InvokeAsync();
else
this.NavManager.NavigateTo("/");
}
/// IDisosable Interface implementation
public void Dispose()
=> this.Service.ListHasChanged -= OnListChanged;
}
分页和排序由驻留在ControllerService中的Paginator类实现。有与Paginator:PaginatorControl和SortControl交互的UI组件。
您可以在列表表单中看到PaginatorControl在使用——此处位于表单底部按钮行的左侧
New Record
Exit
并且SortControl在列表表单的标题行中起作用。
ID
Date
...
控制器服务持有列表表单使用的Paginator实例。代码是不言自明的,提供了分页操作的功能。它被传递给数据服务以通过PaginatorData类检索正确的排序页面。
public class Paginator
{
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 25;
public int BlockSize { get; set; } = 10;
public int RecordCount { get; set; } = 0;
public event EventHandler PageChanged;
public string SortColumn
{
get => (!string.IsNullOrWhiteSpace(_sortColumn)) ? _sortColumn : DefaultSortColumn;
set => _sortColumn = value;
}
private string _sortColumn = string.Empty;
public string DefaultSortColumn { get; set; } = "ID";
public bool SortDescending { get; set; }
public int LastPage => (int)((RecordCount / PageSize) + 0.5);
public int LastBlock => (int)((LastPage / BlockSize) + 1.5);
public int CurrentBlock => (int)((Page / BlockSize) + 1.5);
public int StartBlockPage => ((CurrentBlock - 1) * BlockSize) + 1;
public int EndBlockPage => StartBlockPage + BlockSize;
public bool HasBlocks => ((RecordCount / (PageSize * BlockSize)) + 0.5) > 1;
public bool HasPagination => (RecordCount / PageSize) > 1;
public Paginator(int pageSize, int blockSize)
{
this.BlockSize = blockSize;
this.PageSize = pageSize;
}
public void ToPage(int page, bool forceUpdate = false)
{
if ((forceUpdate | !this.Page.Equals(page)) && page > 0)
{
this.Page = page;
this.PageChanged?.Invoke(this, EventArgs.Empty);
}
}
public void NextPage()
=> this.ToPage(this.Page + 1);
public void PreviousPage()
=> this.ToPage(this.Page - 1);
public void ToStart()
=> this.ToPage(1);
public void ToEnd()
=> this.ToPage((int)((RecordCount / PageSize) + 0.5));
public void NextBlock()
{
if (CurrentBlock != LastBlock)
{
var calcpage = (CurrentBlock * PageSize * BlockSize) + 1;
this.Page = calcpage > LastPage ? LastPage : LastPage;
this.PageChanged?.Invoke(this, EventArgs.Empty);
}
}
public void PreviousBlock()
{
if (CurrentBlock != 1)
{
this.Page = ((CurrentBlock - 1) * PageSize * BlockSize) - 1;
this.PageChanged?.Invoke(this, EventArgs.Empty);
}
}
public void NotifySortingChanged()
=> this.ToPage(1, true);
public PaginatorData GetData => new PaginatorData()
{
Page = this.Page,
PageSize = this.PageSize,
BlockSize = this.BlockSize,
RecordCount = this.RecordCount,
SortColumn = this.SortColumn,
SortDescending = this.SortDescending
};
}
这是用于将数据传递到dat服务的类。这必须通过json通过api传递,所以“保持简单”/
public class PaginatorData
{
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 25;
public int BlockSize { get; set; } = 10;
public int RecordCount { get; set; } = 0;
public string SortColumn { get; set; } = string.Empty;
public bool SortDescending { get; set; } = false;
}
代码再次是不言自明的,构建了一个Bootstrap ButtonGroup。我一直避免使用图标,如果你愿意,你可以。
@namespace Blazor.SPA.Components
@if (this.hasPaginator)
{
-
|@pageNo
}
}
@if (this.Paginator.HasBlocks)
{
-
>>
}
-
>|
}
@code {
[Parameter] public Paginator Paginator { get; set; }
private bool hasPaginator => this.Paginator != null && this.Paginator.HasPagination;
}
SortControl是在列表头使用。它自身级联并通过一组公共辅助方法为标题列提供到分页器的接口。
@namespace Blazor.SPA.Components
@ChildContent
@code {
[Parameter] public RenderFragment ChildContent { get; set; }
[Parameter] public string NotSortedClass { get; set; } = "sort-column oi oi-resize-height";
[Parameter] public string AscendingClass { get; set; } = "sort-column oi oi-sort-ascending";
[Parameter] public string DescendingClass { get; set; } = "sort-column oi oi-sort-descending";
[Parameter] public EventCallback Sort { get; set; }
[Parameter] public Paginator Paginator { get; set; }
public string SortColumm { get; private set; } = string.Empty;
public bool Descending { get; private set; } = false;
public string GetIcon(string columnName)
=> !this.SortColumm.Equals(columnName)
? this.NotSortedClass
: this.Descending
? this.AscendingClass
: this.DescendingClass;
public void NotifySortingChanged(string sortColumn, bool descending = false)
{
this.SortColumm = sortColumn;
this.Descending = descending;
this.Notify();
}
public void NotifySortingDirectionChanged()
{
this.Descending = !this.Descending;
this.Notify();
}
private void Notify()
{
if (Paginator != null)
{
Paginator.SortDescending = this.Descending;
Paginator.SortColumn = this.SortColumm;
Paginator.NotifySortingChanged();
}
var args = SortingEventArgs.Get(this.SortColumm, this.Descending);
if (Sort.HasDelegate) this.Sort.InvokeAsync(args);
}
}
这是在列表中构建每个标题列的UI控件。它为标题构建了razor和Css类,并在任何鼠标单击事件上通知捕获的SortControl。
@namespace Blazor.SPA.Components
@if (_isSortField)
{
@this.ChildContent
}
else
{
@this.ChildContent
}
@code {
[CascadingParameter] public SortControl SortControl { get; set; }
[Parameter] public RenderFragment ChildContent { get; set; }
[Parameter] public string SortField { get; set; } = string.Empty;
[Parameter(CaptureUnmatchedValues = true)] public IDictionary UserAttributes { get; set; } = new Dictionary();
private bool _hasSortControl => this.SortControl != null;
private bool _isSortField => !string.IsNullOrWhiteSpace(this.SortField);
private string _iconclass => _hasSortControl && _isSortField ? this.SortControl.GetIcon(SortField) : string.Empty;
private string CssClass => CSSBuilder.Class("grid-col")
.AddClass("cursor-hand", _isSortField)
.AddClassFromAttributes(this.UserAttributes)
.Build();
private void SortClick(MouseEventArgs e)
{
if (this.SortControl.SortColumm.Equals(this.SortField))
this.SortControl.NotifySortingDirectionChanged();
else
this.SortControl.NotifySortingChanged(this.SortField);
}
}
解决方案中有三种列表表单。它们展示了不同的UI方法。
- 为记录查看器和编辑器使用不同的RouteView(页面)的经典网页方法。
- 模态对话框方法——在列表RouteView中打开和关闭模态对话框。
- 内嵌对话方法——在RouteView中打开和关闭一个部分以显示/编辑记录。
标准WeatherForecastListForm看起来像这样。它继承自ListFormBase, WeatherForecast作为 TRecord。它将WeatherForecastControllerService分配给基接口IFactoryControllerService属性Service。请注意,它有一个组件Css文件,用于定义组件中使用的自定义Css。
// Blazor.Database/Components/Forms/WeatherForecast/WeatherForecastListForm.razor.cs
public partial class WeatherForecastListForm : ListFormBase
{
[Inject] private WeatherForecastControllerService ControllerService { get; set; }
[Parameter] public bool IsModal {get; set;}
private BaseModalDialog Modal { get; set; }
protected override async Task OnInitializedAsync()
{
this.Service = this.ControllerService;
await base.OnInitializedAsync();
}
protected override async void Edit(int id)
{
if (this.IsModal)
{
var options = new ModalOptions();
options.Set("Id", id);
await this.Modal.ShowAsync(options);
}
else
base.Edit(id);
}
protected override async void View(int id)
{
if (this.IsModal)
{
var options = new ModalOptions();
options.Set("Id", id);
await this.Modal.ShowAsync(options);
}
else
base.View(id);
}
protected override async void New()
{
if (this.IsModal)
{
var options = new ModalOptions();
options.Set("Id", -1);
await this.Modal.ShowAsync(options);
}
else
base.New();
}
}
razor标记。注意:
- 头部中的SortControl和使用排序构建头部的UIDataTableHeaderColumn组件。
- 链接到Service.Paginator的按钮行中的PaginatorControl。分页是事件驱动的。PaginatorControl分页请求由Paginator控制器服务直接处理。更新触发服务中的ListChanged事件,该事件触发列表表单中的UI更新。
- 该表单是否使用模式对话框添加BaseModalDialog。
// Blazor.Database/Components/Forms/WeatherForecast/WeatherForecastListForm.razor.cs
public partial class WeatherForecastListForm : ListFormBase
{
[Inject] private WeatherForecastControllerService ControllerService { get; set; }
[Parameter] public bool IsModal {get; set;}
private BaseModalDialog Modal { get; set; }
protected override async Task OnInitializedAsync()
{
this.Service = this.ControllerService;
await base.OnInitializedAsync();
}
protected override async void Edit(int id)
{
if (this.IsModal)
{
var options = new ModalOptions();
options.Set("Id", id);
await this.Modal.ShowAsync(options);
}
else
base.Edit(id);
}
protected override async void View(int id)
{
if (this.IsModal)
{
var options = new ModalOptions();
options.Set("Id", id);
await this.Modal.ShowAsync(options);
}
else
base.View(id);
}
protected override async void New()
{
if (this.IsModal)
{
var options = new ModalOptions();
options.Set("Id", -1);
await this.Modal.ShowAsync(options);
}
else
base.New();
}
}
应用程序为列表表单声明了一组中间视图。这些对于WASM和服务器SPA都是通用的。
WeatherForecastComponent这是多RouteView实现。事件处理程序连接到WeatherForecastListForm,通过NavigationManager路由到不同的RouteView。
@namespace Blazor.Database.Components
@code {
[Inject] NavigationManager NavManager { get; set; }
protected override Task OnInitializedAsync()
{
return base.OnInitializedAsync();
}
public void GoToEditor(int id)
=> this.NavManager.NavigateTo($"/weather/edit/{id}");
public void GoToNew()
=> this.NavManager.NavigateTo($"/weather/edit/-1");
public void GoToViewer(int id)
=> this.NavManager.NavigateTo($"/weather/view/{id}");
}
模态实现很简单。它已经通过启用IsModal.。您实际上并不需要它,因为您可以直接在RouteView中声明WeatherForecastListForm。
@namespace Blazor.Database.Components
内联对话框是最复杂的。它使用Id通过UIBase显示/隐藏编辑器/查看器。
@namespace Blazor.Database.Components
@code {
[Inject] NavigationManager NavManager { get; set; }
private int editorId = 0;
private int viewerId = 0;
private bool ShowViewer => this.viewerId != 0;
private bool ShowEditor => this.editorId != 0;
public void GoToEditor(int id)
=> SetIds(id, 0);
public void GoToNew()
=> SetIds(-1, 0);
public void GoToViewer(int id)
=> SetIds(0, id);
public void CloseDialog()
=> SetIds(0, 0);
public void Exit()
=> this.NavManager.NavigateTo("/");
private void SetIds(int editorId, int viewerId)
{
this.editorId = editorId;
this.viewerId = viewerId;
}
}
RouteViews(又名页面)
这些只是声明路由和顶级表单组件。
- Blazor.Database.WASM/RouteViews/Weather/xxx.razor
- Blazor.Database.Server/RouteViews/Weather/xxx.razor
@page "/fetchdata"
@page "/fetchdataInline"
@page "/fetchdataModal"
总结
这篇文章到此结束。需要注意的一些关键点:
- Blazor服务器和Blazor WASM代码库之间没有区别。
- 90%以上的功能在库组件中作为样板通用代码实现。大多数应用程序代码是用于单个记录表单的Razor标记。
- 异步功能贯穿始终。
如果您在未来阅读本文,请查看存储库中的自述文件以获取文章集的最新版本。
Building a Database Application in Blazor - Part 5 - View Components - CRUD List Operations in the UI - CodeProject