简单的Blazor模式对话框实现

目录

代码仓库

实现

概述

IModalOptions

ModalOptions

ModalResult

IModalDialogContext

ModalDialogContext

ModalDialogBase

VanillaModalDialog

BsModelDialog

示范

WeatherEditForm

总结


代码仓库

代码仓库在这里:模态对话框仓库

实现

概述

该实现由两个接口、四个类和一个enum

  1. IModalOptions

  2. IModalDialogContext

  3. ModalOptions

  4. ModalResult

  5. ModalResultType

  6. ModalDialogContext

  7. ModalDialogBase

示例代码使用标准Blazor模板,并演示如何从FetchData中的WeatherForecast列表中打开编辑窗体组件。

下面的代码演示了基础知识:如何在模式对话框中打开WeatherEditForm。该方法生成一个包含记录UidIModalOptions对象。它调用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:

  1. 确保传递的类型是一个组件,即实现IComponent

  2. 设置状态。

  3. 调用回调以通知组件渲染:这将显示对话框框架并创建内容组件。

  4. 使用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:

  1. 清除状态。

  2. 调用回调以通知组件渲染:这将隐藏对话框框架并销毁内容组件。

  3. TaskCompletionSource设置为完成。如果调用方等待Show,则调用方法现在将运行完成。

private void CloseModal(ModalResult result)
{
    this.Display = false;
    this.ModalContentType = null;
    this.NotifyRenderRequired?.Invoke();
    _ = this._ModalTask.TrySetResult(result);
}

Switch:

  1. 设置状态。

  2. 调用回调以通知组件进行渲染:这将显示带有新内容组件的对话框架。

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样式模式对话框组件包装器。它有:

  1. 可点击的背景

  2. 可配置的宽度

  3. 使用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

WeatherEditFormWeatherForecast记录的编辑表单。

它:

  1. 捕获级联的IModalDialogContext

  2. 如果没有级联IModalDialogContext,则抛出期望:表单设计为在模态对话框上下文中运行。

  3. 使用EditStateTracker。这将跟踪编辑状态,并在此处详细介绍 Blazr.EditStateTracker

  4. 保存关闭”中与模态上下文进行交互。

// 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 &deg;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 &deg;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

  1. 向每一行添加一个编辑按钮。

  2. BsModalDialog组件添加到页面。

  3. 在模式组件上调用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组件的多种技术和做法。

  1. 如何使用TaskCompletionSource来管理显示和隐藏对话框。

  2. 将组件状态分离到上下文类中,以便可以级联状态上下文而不是组件。

  3. 示例代码演示了编辑状态跟踪和导航锁定。

https://www.codeproject.com/Articles/5286721/A-Simple-Blazor-Modal-Dialog-Implementation

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值