目录
代码仓库
代码仓库在这里:模态对话框仓库
实现
概述
该实现由两个接口、四个类和一个enum:
-
IModalOptions
-
IModalDialogContext
-
ModalOptions
-
ModalResult
-
ModalResultType
-
ModalDialogContext
-
ModalDialogBase
示例代码使用标准Blazor模板,并演示如何从FetchData中的WeatherForecast列表中打开编辑窗体组件。
下面的代码演示了基础知识:如何在模式对话框中打开WeatherEditForm。该方法生成一个包含记录Uid的IModalOptions对象。它调用ShowAsync<WeatherForm>(options),定义要显示的组件表单以及该表单的选项,并等待返回的Task。在模态关闭之前Task不会完成。
private async Task EditAsync(Guid uid)
{
if (_modal is not null)
{
var options = new BsModalOptions();
options.ControlParameters.Add("Uid", uid);
var result = await _modal.Context.ShowAsync<WeatherEditForm>(options);
// Code to run after the Dialog closes
}
}
表单调用Close(modal result)完成Task, EditAsync运行到完成。
private void Close()
{
this.Modal?.Close(ModalResult.OK());
}
IModalOptions
IModalOptions定义将数据传递到对话框的三种方法。模态对话框实现可以使用泛型ModalOptions或定义特定的IModalOptions。
public interface IModalOptions
{
public Dictionary<string, object> ControlParameters { get; }
public Dictionary<string, object> OptionsList { get; }
public object Data { get; }
}
ModalOptions
IModalOptions的基本实现。
public class ModalOptions: IModalOptions
{
public Dictionary<string, object>
ControlParameters { get; } = new Dictionary<string, object>();
public Dictionary<string, object> OptionsList { get; } =
new Dictionary<string, object>();
public object Data { get; set; } = new();
}
ModalResult
ModalResult是向调用方提供状态和数据的返回记录。
public sealed record ModalResult
{
public ModalResultType ResultType { get; private set; } = ModalResultType.NoSet;
public object? Data { get; set; } = null;
public static ModalResult OK() => new ModalResult()
{ ResultType = ModalResultType.OK };
//... lots of static constructors
}
和ModalResultType。
public enum ModalResultType { NoSet, OK, Cancel, Exit }
IModalDialogContext
ModalDialogContext将模式对话框组件的状态和状态管理封装在上下文类中。
IModalDialogContext定义接口。
public interface IModalDialogContext
{
public IModalOptions? Options { get; }
public bool Display { get; }
public bool IsActive { get; }
public Type? ModalContentType { get; }
public Action? NotifyRenderRequired { get; set; }
public Task<ModalResult> ShowAsync<TModal>(IModalOptions options)
where TModal : IComponent;
public Task<ModalResult> ShowAsync(Type control, IModalOptions options);
public bool Switch<TModal>(IModalOptions options) where TModal : IComponent;
public bool Switch(Type control, IModalOptions options);
public void Update(IModalOptions? options = null);
public void Dismiss();
public void Close(ModalResult result);
}
ModalDialogContext
ModalDialogContext实现了IModalDialogContext,为ModalDialog实现提供样板代码。
它由用于维护状态的属性和用于显示、隐藏、切换和重置组件内容的方法组成。
Show:
-
确保传递的类型是一个组件,即实现IComponent。
-
设置状态。
-
调用回调以通知组件渲染:这将显示对话框框架并创建内容组件。
-
使用TaskCompletionSource构造一个手动活动任务,并将该任务传回给调用方await。
protected TaskCompletionSource<ModalResult> _ModalTask
{ get; set; } = new TaskCompletionSource<ModalResult>();
private Task<ModalResult> ShowModalAsync(Type control, IModalOptions options)
{
if (!(typeof(IComponent).IsAssignableFrom(control)))
throw new InvalidOperationException
("Passed control must implement IComponent");
this.Options = options;
this.ModalContentType = control;
this.Display = true;
this.NotifyRenderRequired?.Invoke();
this._ModalTask = new TaskCompletionSource<ModalResult>();
return this._ModalTask.Task;
}
Close:
-
清除状态。
-
调用回调以通知组件渲染:这将隐藏对话框框架并销毁内容组件。
-
将TaskCompletionSource设置为完成。如果调用方等待Show,则调用方法现在将运行完成。
private void CloseModal(ModalResult result)
{
this.Display = false;
this.ModalContentType = null;
this.NotifyRenderRequired?.Invoke();
_ = this._ModalTask.TrySetResult(result);
}
Switch:
-
设置状态。
-
调用回调以通知组件进行渲染:这将显示带有新内容组件的对话框架。
private async Task<bool> SwitchModalAsync(Type control, IModalOptions options)
{
if (!(typeof(IComponent).IsAssignableFrom(control)))
throw new InvalidOperationException("Passed control must implement IComponent");
this.ModalContentType = control;
this.Options = options;
await this.InvokeAsync(StateHasChanged);
return true;
}
全部的类:
public class ModalDialogContext : IModalDialogContext
{
public IModalOptions? Options { get; protected set; }
public bool Display { get; protected set; }
public bool IsActive => this.ModalContentType is not null;
public Action? NotifyRenderRequired { get; set; }
private TaskCompletionSource<ModalResult> _ModalTask
{ get; set; } = new TaskCompletionSource<ModalResult>();
public Type? ModalContentType {get; private set;} = null;
public Task<ModalResult> ShowAsync<TModal>(IModalOptions options)
where TModal : IComponent
=> this.ShowModalAsync(typeof(TModal), options);
public Task<ModalResult> ShowAsync(Type control, IModalOptions options)
=> this.ShowModalAsync(control, options);
public bool Switch<TModal>(IModalOptions options) where TModal : IComponent
=> this.SwitchModal(typeof(TModal), options);
public bool Switch(Type control, IModalOptions options)
=> this.SwitchModal(control, options);
public void Update(IModalOptions? options = null)
{
this.Options = options ?? this.Options;
this.NotifyRenderRequired?.Invoke();
}
public void Dismiss()
=> this.CloseModal(ModalResult.Cancel());
public void Close(ModalResult result)
=> this.CloseModal(result);
private Task<ModalResult> ShowModalAsync(Type control, IModalOptions options)
{
if (!(typeof(IComponent).IsAssignableFrom(control)))
throw new InvalidOperationException
("Passed control must implement IComponent");
this.Options = options;
this.ModalContentType = control;
this.Display = true;
this.NotifyRenderRequired?.Invoke();
this._ModalTask = new TaskCompletionSource<ModalResult>();
return this._ModalTask.Task;
}
private bool SwitchModal(Type control, IModalOptions options)
{
if (!(typeof(IComponent).IsAssignableFrom(control)))
throw new InvalidOperationException
("Passed control must implement IComponent");
this.ModalContentType = control;
this.Options = options;
this.NotifyRenderRequired?.Invoke();
return true;
}
private void CloseModal(ModalResult result)
{
this.Display = false;
this.ModalContentType = null;
this.NotifyRenderRequired?.Invoke();
_ = this._ModalTask.TrySetResult(result);
}
}
ModalDialogBase
ModalDialogBase实现模式对话框组件的样板代码。
它创建一个ModalDialogContext实例并在SetParametersAsync中设置回调:这确保了继承类不会无意中覆盖它。
public abstract class ModalDialogBase : ComponentBase
{
public readonly IModalDialogContext Context = new ModalDialogContext();
public override Task SetParametersAsync(ParameterView parameters)
{
parameters.SetParameterProperties(this);
this.Context.NotifyRenderRequired = this.OnRenderRequested;
return base.SetParametersAsync(ParameterView.Empty);
}
private void OnRenderRequested()
=> StateHasChanged();
}
VanillaModalDialog
VanillaModalDialog提供基本的CSS样式模式对话框组件包装器。它有:
-
可点击的背景
-
可配置的宽度
-
使用DynamicComponent渲染请求的组件
VanillaModalDialog.razor
@namespace Blazr.ModalDialog.Components
@inherits ModalDialogBase
@implements IModalDialog
@if (this.Display)
{
<CascadingValue Value="(IModalDialog)this">
<div class="base-modal-background" @onclick="OnBackClick">
<div class="base-modal-content" style="@this.Width"
@onclick:stopPropagation="true">
<DynamicComponent Type=this.ModalContentType
Parameters=this.Options?.ControlParameters />
</div>
</div>
</CascadingValue>
}
@code {
private VanillaModalOptions modalOptions =>
this.Options as VanillaModalOptions ?? new();
protected string Width
=> string.IsNullOrWhiteSpace(modalOptions.ModalWidth) ?
string.Empty : $"width:{modalOptions.ModalWidth}";
private void OnBackClick()
{
if (modalOptions.ExitOnBackgroundClick)
this.Close(ModalResult.Exit());
}
}
VanillaModalDialog.razor.css:
div.base-modal-background {
display: block;
position: fixed;
z-index: 101; /* Sit on top */
left: 0;
top: 0;
width: 100%; /* Full width */
height: 100%; /* Full height */
overflow: auto; /* Enable scroll if needed */
background-color: rgb(0,0,0); /* Fallback color */
background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
}
div.base-modal-content {
background-color: #fefefe;
margin: 10% auto;
padding: 10px;
border: 2px solid #888;
width: 90%;
}
BsModelDialog
BsModalDialog提供Bootstrap样式的模态对话框组件包装器。
它有一个自定义IModalOptions,您可以在其中设置模态大小。
public sealed class BsModalOptions: IModalOptions
{
public string ModalSize { get; set; } = "modal-xl";
public Dictionary<string, object> ControlParameters { get; } =
new Dictionary<string, object>();
public Dictionary<string, object> OptionsList { get; } =
new Dictionary<string, object>();
public object Data { get; set; } = new();
}
BsModalDialog.razor
@namespace Blazr.ModalDialog.Components
@inherits ModalDialogBase
@if (this.Context.Display)
{
<CascadingValue Value="(IModalDialogContext)this.Context">
<div class="modal show-modal" tabindex="-1">
<div class="modal-dialog @this.Size">
<div class="modal-content">
<div class="modal-body">
<DynamicComponent Type=this.Context.ModalContentType
Parameters=this.Context.Options?.ControlParameters />
</div>
</div>
</div>
</div>
</CascadingValue>
}
@code {
private BsModalOptions modalOptions =>
this.Context.Options as BsModalOptions ?? new();
protected string Size => modalOptions.ModalSize;
}
和BsModalDialog.razor.css:
.modal-body {
padding: 0;
}
.show-modal {
display: block;
background-color: rgb(0,0,0,0.6);
}
示范
该演示使用该FetchData页面,为天气预报添加模态对话框编辑器。您可以查看存储库中的所有代码,包括更新的WeatherForecastService。
WeatherEditForm
WeatherEditForm是WeatherForecast记录的编辑表单。
它:
-
捕获级联的IModalDialogContext。
-
如果没有级联IModalDialogContext,则抛出期望:表单设计为在模态对话框上下文中运行。
-
使用EditStateTracker。这将跟踪编辑状态,并在此处详细介绍 Blazr.EditStateTracker。
-
在“保存并关闭”中与模态上下文进行交互。
// WeatherEditForm.razor
@inject WeatherForecastService DataService
<div class="p-3">
<div class="mb-3 display-6 border-bottom">
Weather Forecast Editor
</div>
<EditForm Model=this.model OnSubmit=this.SaveAsync>
<DataAnnotationsValidator/>
<EditStateTracker LockNavigation EditStateChanged=this.OnEditStateChanged />
<div class="mb-3">
<label class="form-label">Date</label>
<InputDate class="form-control" @bind-Value=this.model.Date />
</div>
<div class="mb-3">
<label class="form-label">Temperature °C</label>
<InputNumber class="form-control" @bind-Value=this.model.TemperatureC />
</div>
<div class="mb-3">
<label class="form-label">Summary</label>
<InputSelect class="form-select" @bind-Value=this.model.Summary>
@if (model.Summary is null)
{
<option disbabled selected value="null">
-- Select a Summary -- </option>
}
@foreach (var summary in this.DataService.Summaries)
{
<option value="@summary">@summary</option>
}
</InputSelect>
</div>
<div class="mb-3 text-end">
<button disabled="@(!_isDirty)" type="submit"
class="btn btn-primary" @onclick=SaveAsync>Save</button>
<button disabled="@_isDirty" type="button"
class="btn btn-dark" @onclick=Close>Exit</button>
</div>
</EditForm>
</div>
<div class="bg-dark text-white m-4 p-2">
<pre>Date : @this.model.Date</pre>
<pre>Temperature °C : @this.model.TemperatureC</pre>
<pre>Summary: @this.model.Summary</pre>
<pre>State: @(_isDirty ? "Dirty" : "Clean")</pre>
</div>
@code {
[Parameter] public Guid Uid { get; set; }
[CascadingParameter] private IModalDialogContext? Modal { get; set; }
private WeatherForecast model = new();
private bool _isDirty;
protected override async Task OnInitializedAsync()
{
ArgumentNullException.ThrowIfNull(Modal);
model = await this.DataService.GetForecastAsync(this.Uid) ?? new()
{ Date = DateOnly.FromDateTime(DateTime.Now), TemperatureC = 10 };
}
private void OnEditStateChanged(bool isDirty)
=> _isDirty = isDirty;
private async Task SaveAsync()
{
await this.DataService.SaveForecastAsync(model);
this.Modal?.Close(ModalResult.OK());
}
private void Close()
=> this.Modal?.Close(ModalResult.OK());
}
和FetchData
-
向每一行添加一个“编辑”按钮。
-
将BsModalDialog组件添加到页面。
-
在模式组件上调用ShowAsync以打开带有“编辑窗体”的模式对话框。
@page "/fetchdata"
@using Blazr.ModalDialog.Data
@inject WeatherForecastService ForecastService
<PageTitle>Weather forecast</PageTitle>
<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from a service.</p>
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
<td><button class="btn btn-sm btn-primary"
@onclick="() => EditAsync(forecast.Uid)">Edit</button></td>
</tr>
}
</tbody>
</table>
<BsModalDialog @ref=_modal />
@code {
private IEnumerable<WeatherForecast> forecasts =
Enumerable.Empty<WeatherForecast>();
private BsModalDialog? _modal;
protected override async Task OnInitializedAsync()
{
forecasts = await ForecastService.GetForecastAsync();
}
private async Task EditAsync(Guid uid)
{
if (_modal is not null)
{
var options = new BsModalOptions();
options.ControlParameters.Add("Uid", uid);
var result = await _modal.Context.ShowAsync<WeatherEditForm>(options);
}
}
}
总结
此实现演示了开发Blazor组件的多种技术和做法。
-
如何使用TaskCompletionSource来管理显示和隐藏对话框。
-
将组件状态分离到上下文类中,以便可以级联状态上下文而不是组件。
-
示例代码演示了编辑状态跟踪和导航锁定。
https://www.codeproject.com/Articles/5286721/A-Simple-Blazor-Modal-Dialog-Implementation