I am new to Blazor and a replacement for JavaScript Alert, Confirm, and Prompt was one of the first things on my list. I came up with a service in Blazor Server / BlazorWebView (I haven't tested in Web Assembly). The <Modal>
component we'll create can be controlled by the service or directly from JavaScript. Although, if you don't need to call the <Modal>
via JavaScript than you can remove any JavaScript or IJSRuntime
references.
The ModalService.cs
is very simple. It has an OnShow
event that we can hook into our <Modal>
component later. The event is a function that takes prameters ModalType, title, body and it returns a dynamic task.
Setup
ModalService.cs
namespace MyProjectName.Services
{
public class ModalService
{
public event Func<ModalBase.ModalType, string, string,Task<dynamic>> OnShow;
public async Task<dynamic> Show(ModalBase.ModalType mType, string title, string body)
{
if(OnShow != null)
return await OnShow?.Invoke(mType, title, body);
return null;
}
}
}
The ModalBase.cs
will be inherited by our <Modal>
component. It handles opening and closing the modal. Here is where we can also attach to the ModalService
event OnShow
and hook up support for JavaScript invoking.
ModalBase.cs
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
namespace MyProjectName.Services
{
public class ModalBase : ComponentBase, IDisposable
{
[Inject] ModalService ModalService { get; set; }
[Inject] IJSRuntime JS { get; set; }
public enum ModalType
{
Alert,
Prompt,
Confirm
}
protected override void OnInitialized()
{
// Attach to our service event.
ModalService.OnShow += Show;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
// Set a JavaScript referene for our DotNet interop.
if(firstRender)
await JS.InvokeVoidAsync("MODAL.SetDotnetReference", DotNetObjectReference.Create(this));
}
public string Title { get; set; }
public string Body { get; set; }
public Guid Guid = Guid.NewGuid();
public string ModalDisplay = "none;";
public string ModalClass = "";
public bool ShowBackdrop = false;
public string PromptValue { get; set; }
private bool ConfirmValue { get; set; }
public ModalType MType { get; set; }
private List<string> MsgIds = new List<string>();
[JSInvokable("Show")]
public async Task<dynamic> Show(ModalType mType, string title, string body)
{
// The JavaScript call MODAL.DotNetReference.invokeMethodAsync is non-blocking
// This means multiple calls to show the modal using invokeMethodAsync will only show the modal once.
// We can solve this by making sure each message waits in line.
string msgId = Guid.NewGuid().ToString();
if (!MsgIds.Contains(msgId))
MsgIds.Add(msgId);
// If multiple messages are being processed, wait for this msgs turn.
while (MsgIds.Count > 1 && MsgIds.IndexOf(msgId) != 0)
await Task.Delay(250);
Title = title;
Body = body;
ModalDisplay = "block;";
ModalClass = "Show";
MType = mType;
ShowBackdrop = true;
StateHasChanged();
while (ShowBackdrop)
await Task.Delay(250);
switch (mType)
{
default:
case ModalType.Alert:
MsgIds.Remove(msgId);
return string.Empty;
case ModalType.Confirm:
bool confirmResponse = ConfirmValue;
MsgIds.Remove(msgId);
return confirmResponse;
case ModalType.Prompt:
string promptResponse = PromptValue;
MsgIds.Remove(msgId);
return promptResponse;
}
}
public void Close(bool isCancel)
{
// Determine returned values.
PromptValue = isCancel ? string.Empty : PromptValue;
ConfirmValue = isCancel ? false : true;
ModalDisplay = "none";
ModalClass = "";
ShowBackdrop = false;
StateHasChanged();
}
public void Dispose()
{
ModalService.OnShow -= Show;
}
}
}
I designed the <Modal>
component based off the bootstrap markup discussed here. The major difference is I that have moved the guts into ModalBase.cs
to interact with our service.
Modal.razor
@using Microsoft.JSInterop
@using MyProjectName.Services
@inherits ModalBase
<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 w-100 text-center" style="padding-left:31px">@Title</h5>
<button type="button" class="close border-0 bg-white" data-dismiss="modal" aria-label="Close" @onclick="() => Close(true)">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body mx-auto text-center text-break">
@Body
@if (MType == ModalType.Prompt){
<input type="text" class="form-control text-center my-2" @bind-value="PromptValue" style="max-width:400px"></input>
}
</div>
<div class="modal-footer justify-content-center">
@if (MType == ModalType.Prompt || MType == ModalType.Confirm)
{
<button type="button" class="btn btn-secondary" data-dismiss="modal" @onclick="() => Close(false)">OK</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal" @onclick="() => Close(true)">Cancel</button>
}
else
{
<button type="button" class="btn btn-secondary" data-dismiss="modal" @onclick="() => Close(false)">Close</button>
}
</div>
</div>
</div>
</div>
@if (ShowBackdrop)
{
<div class="modal-backdrop fade show"></div>
}
Usage
Include the ModalService
into our service collection.
Program.cs
builder.Services.AddScoped<ModalService>();
MainLayout.razor
@using MyProjectName.Components
@inherits LayoutComponentBase
<PageTitle>My Project</PageTitle>
<Modal></Modal>
<div class="page">
.
.
.
</div>
Inject and use the service somewhere in your application.
Index.razor
@code
{
[Inject] public ModalService ModalService { get; set; }
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
if (await ModalService.Show(Modal.ModalType.Confirm,"Save Settings", "Are you sure you want to save settings?"))
{
string fileName = await ModalService.Show(Modal.ModalType.Prompt, "File Name", "Please enter a filename");
if (!string.IsNullOrEmpty(fileName))
await ModalService.Show(Modal.ModalType.Alert, "File Saved Success", $"File Saved as {fileName}");
else
await ModalService.Show(Modal.ModalType.Alert, "File Saved Cancelled", $"No file name was entered.");
}
}
// return base.OnAfterRenderAsync(firstRender);
}
}
JavaScript Usage
// Defined somewhere globally
var MODAL = {};
MODAL.DotNetReference = null;
MODAL.SetDotnetReference = function (pDotNetReference) {
MODAL.DotNetReference = pDotNetReference;
};
MODAL.MType = {
Alert: 0,
Prompt:1,
Confirm: 2,
};
// Called from wherever
MODAL.DotNetReference.invokeMethodAsync('Show', MODAL.MType.Prompt, `Title goes here`, `Body goes here`)
.then(data => {
console.log(`Prompt Response`, data);
});
JavaScript Note: Polyfil recommended for promise support in older browsers
Note: If you need to show the modal at earlier points in the application lifecycle, such as OnInitializedAsync
, than you'll need to change ServerPrerendered
to Server
.
@*<component type="typeof(App)" render-mode="ServerPrerendered" />*@
<component type="typeof(App)" render-mode="Server" />