An awaitable Blazor Modal Dialog

11/11/2024
6 minute read

I already showcased a simple modal dialog in a previous post. However, the dialog was not awaitable. In this post, I'll show you how to create an awaitable modal dialog.

An awaitable modal dialog

While callbacks are very nice, sometimes it is just way more convenient to await a dialog. Something likes this:

var result = await MyDialog.ShowAsync();
if (result is null)
{
    // The user pressed "Cancel"
}
else
{
    // The user pressed "Ok"
}

To make this work, we can utilize the power of TaskCompletionSource<T>. This class allows us to create a Task<T> that we can complete whenever we want. This is perfect for our use case. I also have a blog post about this: "Wrap Event based functions into awaitable Tasks - Meet TaskCompletionSource".

The Dialog

I used this recently in the following component, that asks the user for a filename and an optional flag:

<div class="modal @modalClass" tabindex="-1" role="dialog" style="display:@modalDisplay; overflow-y: auto;">
    <div class="modal-dialog modal-lg" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">Configuration</h5>
                <button type="button" class="btn-close" @onclick="OnAbort"></button>
            </div>
            <div class="modal-body">
                <EditForm Model="@model" OnValidSubmit="OnOk">
                    <DataAnnotationsValidator />
                    <div class="form-floating mb-3">
                        <InputText type="text" class="form-control" id="name" placeholder="Filename"
                            @bind-Value="model.Name" />
                        <label for="name">Filename</label>
                        <ValidationMessage For="() => model.Name"></ValidationMessage>
                    </div>
                    <div class="form-check form-switch mb-3">
                        <InputCheckbox class="form-check-input" id="cache" @bind-Value="model.CacheMedia" />
                        <label class="form-check-label" for="cache">Enable Media Caching</label><br />
                        <small class="form-text text-body-secondary">If enabled, the browser will cache the media file</small>
                    </div>
                </EditForm>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" @onclick="OnAbort">Abort</button>
                <button type="button" class="btn btn-primary" @onclick="OnOk">OK</button>
            </div>
        </div>
    </div>
</div>

@if (showBackdrop)
{
    <div class="modal-backdrop fade show"></div>
}

@code {
    private TaskCompletionSource<UploadFileModalDialogObject?> result = null!;
    private string modalDisplay = "none;";
    private string modalClass = "";
    private bool showBackdrop;
    private readonly UploadFileModalDialogObject model = new();

    public async Task<UploadFileModalDialogObject?> ShowAsync(string fileName)
    {
        modalDisplay = "block;";
        modalClass = "show";
        showBackdrop = true;
        model.Name = fileName;
        result = new TaskCompletionSource<UploadFileModalDialogObject?>();
        StateHasChanged();
        return await result.Task;
    }

    private void OnAbort()
    {
        modalDisplay = "none";
        modalClass = "";
        showBackdrop = false;
        result.SetResult(null);
        StateHasChanged();
    }

    private void OnOk()
    {
        modalDisplay = "none";
        modalClass = "";
        showBackdrop = false;
        result.SetResult(model);
        StateHasChanged();
    }
}

It looks like this:

dialog

The interesting bid is the ShowAsync method. It sets up the dialog and returns the Task that we can await. The OnAbort and OnOk methods are called when the user presses the corresponding button. They set the result and close the dialog (with the given result).

As discussed in "Wrap Event based functions into awaitable Tasks - Meet TaskCompletionSource", we can make event driven (aka the button clicks) functions awaitable by using TaskCompletionSource<T>. This is exactly what we did here.

Usage

I have a component where I do open this dialog when the user drag 'n drops a file onto the page:

<UploadFileModalDialog @ref="UploadDialog"></UploadFileModalDialog>

@code {
    private UploadFileModalDialog UploadDialog { get; set; } = default!;

    private async Task OnDrop(InputFileChangeEventArgs e)
    {
        var file = GetFile(e);

        var result = await UploadDialog.ShowAsync(file.Name);
        if (result is null)
        {
            return;
        }

        // Upload file
        ...
    }
}

Conclusion

Creating an awaitable dialog is quite simple. We just need to use a TaskCompletionSource<T> to create a Task<T> that we can complete whenever we want. This allows us to await the dialog and get the result when the user closes the dialog.

Modal Dialog component with Bootstrap in Blazor

This short blog post will show you how to utilize Bootstrap to create a small and reuseable ModalDialogComponent.

UNO Platform - Build a Todo App - Part 3

In the third part of our small mini series: "Building an todo app with the Uno Platform" we will dive deeper into creating a modal dialog where we can enter the details of our todo item. We will see how we can import additional Nuget packages and how we can leverage validation.

Creating a ToolTip Component in Blazor

In this blog post we will create a ToolTip component in Blazor from scratch. We will use the Blazor WebAssembly template to create a new project. We will then add a ToolTip component to the project and use it in the Index page. We will also add some styling to the ToolTip component.

The advantage over using a library is that we can customize the component to our needs as well as keeping it simple! So let's get started!

An error has occurred. This application may no longer respond until reloaded. Reload x