1
votes

I m using VS 2019 and Blazor Server Net5. The included bootstrap 4 environment offers modal dialogs and I would like to replicate something like the MessageBox() from good old Windows Forms. This means you execute (withhin a button click event) something like

DialogResult x = [await] MessageBox[Async]("SomeMsg", SomeButtons+SomeIcon);

And in x you would find what option the user did click (DialogResult.OK, Cancel, ...).

So I found several samples how to show the model dialog itself, and write it even as a component.

I have a simple component MessageBox.razor

public enum ModalResultType { Closed = 0, OK = 1, Cancel = 2, }

@if (ShowMessageBox == true)
{
   <div class="modal fade show d-block" id="MessageBox" tabindex="-1"
      role="dialog" aria-hidden="true">
   .... and so forth ....
   <button type="button" class="close" data-dismiss="modal" aria-label="Close"
      @onclick="() => OnButtonClick(ModalResultType.Closed)">X</button>
   ... and so forth ...
   <button type="button" class="btn btn-primary" data-dismiss="modal"
      @onclick="() => OnButtonClick(ModalResultType.OK)">OK</button>
   <button type="button" class="btn btn-secondary" data-dismiss="modal"
      @onclick="() => OnButtonClick(ModalResultType.Cancel)">Cancel</button>
   .. and so forth ...
   </div>
}

and in the cs behind file I can turn on the component show flag and display the component.

public async Task<ModalResultType> ShowAsync(string title, string messagetext)
{
   Title = title;
   Message = messagetext;
   ShowMessageBox = true;
   StateHasChanged();
   //
   // Now I m at a loss... how to await here what the User did click???
   //
   return whatTheUserDidClick;
}

// Click event from button, called with the appropiate ModalResultType
//
public async Task OnButtonClicked(ModalResultType value)
{
   ShowMessageBox = false;
   //
   // Now I am at a  loss - how to pass the clicked value into the waiting context
   // of the UI above from and "complete" the awaiting ShowAsync();
   //
}

The overall idea is to put into the framing App.Razor-Component this MessageBox component and so every "page" or other component has (via cascading parameter) access to the message box. And if it would need to create a modal MessageBox dialog, for example from some button click event, it could simply do so by calling

[CascadingParameter] public MessageBoxComponent AppRazorMessageComonent {get;set;}

public async Task SomeClickEvent()
{
   // get some form data
   // process them
   // question arises ask user if to proceed or defer
   if (await AppRazorMessageComponent.ShowAsync("Attention", "Shall we proceed?") == ModalResultType.OK)
   {
      // do stuff
   }
   else
   {
      // do other stuff
   }
}

I found sample of modal dialogs where the event handler then is bound right to action to be carried out - like deleting a record. But this is not what I want - I would need to specifically bind the html always to the specifics of the page or component I m in at the time. Or I would need to supply a callback function, which would break my current track; like to set the ShowMessageBox flag, return from the click event, and then proceed along with the logic in another method.

So the question is: how can I await for an event withhin an event handler, which is triggered by an other UI event?

Do I need threads for this - I dont think so. It should be possible by Task, async and await only. But how to create an awaitable object, "signal" the completion, or cancellation, of such a Task? And in a way that it works withhin the Blazor UI component environment.

1
That is a feature for every Blazor component framework. Here is a simple DIY one. - Henk Holterman

1 Answers

2
votes

I made use of the System.Threading.SemaphoreSlim class to acheive the awaitable result in ValueTask<ModalResult<T>> OpenModal() in Modal.cs

BlazorRepl

ModalLauncher.razor

<CascadingValue Value="this">
    @if (ModalContent is not null)
    {
        @ModalContent
    }
    @ChildContent
</CascadingValue>

ModalLauncher.razor.cs

using Microsoft.AspNetCore.Components;

public partial class ModalLauncher : ComponentBase
{
    [Parameter]
    public RenderFragment ChildContent { get; set; }

    public RenderFragment ModalContent { get; set; }

    public void ShowModal(RenderFragment renderFragment)
    {
        ModalContent = renderFragment;
        StateHasChanged();
    }

    public void CloseModal()
    {
        ModalContent = null;
        StateHasChanged();
    }
}

Wrap this around your Layout.

@inherits LayoutComponentBase
<ModalLauncher>
    <div class="page">
        ...
    </div>
</ModalLauncher>

Modal.cs

public class Modal<T> : ComponentBase
{
    [Parameter]
    public RenderFragment<ModalContext<T>> ChildContent { get; set; }

    [Parameter]
    public T Value { get; set; }

    [CascadingParameter]
    public ModalLauncher Launcher { get; set; }

    public async ValueTask<ModalResult<T>> OpenModal(T value)
    {
        var modalContext = new ModalContext<T> { Modal = this, Value = value };
        RenderFragment renderFragment = ChildContent.Invoke(modalContext);
        Launcher.ShowModal(renderFragment);
        await semaphore.WaitAsync();
        return new ModalResult<T> { ModalAction = modalAction, Value = value };
    }

    public void CancelModal() => CloseModal(ModalAction.Cancel);
    public void CloseModal() => CloseModal(ModalAction.Close);
    public void OkModal() => CloseModal(ModalAction.Ok);

    private void CloseModal(ModalAction action)
    {
        modalAction = action;
        Launcher.CloseModal();
        semaphore.Release();
    }

    private ModalAction modalAction;
    private SemaphoreSlim semaphore = new SemaphoreSlim(0, 1);
}
public enum ModalAction
{
    Cancel,
    Close,
    Ok,
}

public class ModalContext<T>
{
    public T Value { get; set; }
    public Modal<T> Modal { get; set; }
}
public class ModalResult<T>
{
    public T Value { get; set; }
    public ModalAction ModalAction { get; set; }
}

public class SomeClass
{
    public int SomeValue { get; set; }
}

Usage Note: When I define the modals I only use a type, They are not bound to an instance. When you call OpenModal(...) you can pass an instance then.

@page "/"

<button @onclick="@OpenSomeClassModal">Run Demo</button>

<Modal @ref="someClassModal" T="SomeClass">
    ...   
    <input type="number" @bind-value="@context.Value.SomeValue" />
    ...
    <button type="button" class="btn btn-secondary" @onclick="@context.Modal.CancelModal">Cancel</button>
    <button type="button" class="btn btn-primary" @onclick="@context.Modal.OkModal">Save changes</button>
    ...             
</Modal>

<Modal @ref="someStringModal" T="string">
    ...
    <p> @context.Value</p>
    ...
    <button type="button" class="btn btn-secondary" @onclick="@context.Modal.OkModal">Close</button>
    ...
</Modal>


@code {
    Modal<SomeClass> someClassModal;
    Modal<string> someStringModal;

    async Task OpenSomeClassModal()
    {
        var someClass = new SomeClass { SomeValue = 9 };
        var result1 = await someClassModal.OpenModal(someClass);
        var result2 = await someStringModal.OpenModal($"The value was set to {result1.Value.SomeValue}, you pressed {result1.ModalAction}");
    }
}

You also need to override part of the bootstrap .modal class. Put this in wwwroot\css\app.css:

.modal {
    display: block;
}