简单的 Blazor 模态对话框实现






4.28/5 (10投票s)
如何为 Blazor 构建模态对话框
代码仓库
代码仓库在此:模态对话框仓库
实现
概述
该实现包含两个接口、四个类和一个 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
,为模态对话框实现提供了样板代码。
它包含用于维护状态的属性以及显示、隐藏、切换和重置组件内容的各种方法。
显示
:
- 确保传入的类型是组件,即实现了
IComponent
。 - 设置状态。
- 调用回调函数通知组件进行渲染:这将显示对话框框架并创建内容组件。
- 使用
TaskCompletionSource
构建一个手动激活的Task,并将该Task返回给调用者进行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
以完成。如果调用者await了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。 - 在Save和Close中与模态框上下文进行交互。
// 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
- 为每行添加一个Edit按钮。
- 将
BsModalDialog
组件添加到页面。 - 调用模态框组件的
ShowAsync
方法以打开模态对话框和Edit表单。
@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
来管理对话框的显示和隐藏。 - 将组件状态分离到上下文类中,以便您可以级联状态上下文而不是组件。
- 示例代码同时演示了编辑状态跟踪和导航锁定。
历史
- 2020年11月19日:初始版本
- 2023年3月25日:修订版2