Click here to Skip to main content
15,881,248 members
Articles / Web Development / Blazor

A Blazor Inline Dialog Control

Rate me:
Please Sign up or sign in to vote.
4.43/5 (3 votes)
17 Mar 2021CPOL4 min read 8.2K   8   3
A Blazor inline dialog control to lock all page controls except those within the form.
The third article in a series describing how to build Blazor edit forms/controls with state management, validation and form locking. This article focuses on form locking.

The Inline Dialog Control

This is the third in a series of articles describing a set of useful Blazor Edit controls that solve some of the current shortcomings in the out-of-the-box edit experience without the need to buy expensive toolkits.

This article describes how to build a component that disables links, buttons the URL bar,... : everywhere except the content within the component. While it can't stop the user navigating through the browser controls, it turns on the browser beforeunload event to force the "Do you really want to leave this site?" dialog box. Everything is implemented with a relatively simple standard Blazor component and a small js file.

Inline Dialog

Links

The repository contains a project that implements the controls for all the articles in this series. You can find it here.

The example site is at https://cec-blazor-database.azurewebsites.net/.

The example form described at this end of this article can be seen at https://cec-blazor-database.azurewebsites.net//inlinedialogeditor.

Previous articles:

Overview

If you want to see the component in action, go to this page on my demo site. It's a basic mockup to demonstrate the functionality and extends the form used in the last two articles. There's a typical Edit Form with the two extra controls covered in the previous articles:

  1. EditFormState monitors the edit state of the Model data
  2. ValidationFormState- a form validator

The key bit of action is hooking up the InlineDialog control Lock to the form state. EditFormState monitors the form state and invokes the EventCallback EditStateChanged whenever a change takes place. The page EditStateChanged event handler is registered with the EditFormState.EditStateChanged and updates _isDirty whenever the state changes. If EditFormState' is dirty, InlineDialog` is locked.

Razor
@using Blazor.Database.Data
@page "/inlinedialogEditor"

    <InlineDialog Lock="this._isDirty" Transparent="false">
        <EditForm Model="@Model" OnValidSubmit="@HandleValidSubmit" class="p-3">
            <EditFormState @ref="editFormState" EditStateChanged="this.EditStateChanged">
            </EditFormState>
            <ValidationFormState @ref="validationFormState"></ValidationFormState>

            <label class="form-label">ID:</label> 
            <InputNumber class="form-control" @bind-Value="Model.ID" />
            <label class="form-label">Date:</label> 
            <InputDate class="form-control" @bind-Value="Model.Date" />
            <ValidationMessage For="@(() => Model.Date)" />
            <label class="form-label">Temp C:</label>
            <InputNumber class="form-control" @bind-Value="Model.TemperatureC" />
            <ValidationMessage For="@(() => Model.TemperatureC)" />
            <label class="form-label">Summary:</label>
            <InputText class="form-control" @bind-Value="Model.Summary" />
            <ValidationMessage For="@(() => Model.Summary)" />

            <div class="mt-2">
                <div>Validation Messages:</div>
                <ValidationSummary />
            </div>

            <div class="text-right mt-2">
                <button class="btn @btnStateColour" disabled>@btnStateText</button>
                <button class="btn @btnValidColour" disabled>@btnValidText</button>
                <button class="btn btn-primary" type="submit" 
                 disabled="@_btnSubmitDisabled">Submit</button>
            </div>

        </EditForm>
    </InlineDialog>
}
C#
@code {
    protected bool _isDirty = false;
    protected bool _isValid => validationFormState?.IsValid ?? true;
    protected string btnStateColour => _isDirty ? "btn-danger" : "btn-success";
    protected string btnStateText => _isDirty ? "Dirty" : "Clean";
    protected string btnValidColour => !_isValid ? "btn-danger" : "btn-success";
    protected string btnValidText => !_isValid ? "Invalid" : "Valid";
    protected bool _btnSubmitDisabled => !(_isValid && _isDirty);

    protected EditFormState editFormState { get; set; }
    protected ValidationFormState validationFormState { get; set; }

    private WeatherForecast Model = new WeatherForecast()
    {
        ID = 1,
        Date = DateTime.Now,
        TemperatureC = 22,
        Summary = "Balmy"
    };

    private void HandleValidSubmit()
    {
        this.editFormState.UpdateState();
    }

    private void EditStateChanged(bool editstate)
        => this._isDirty = editstate;
}

InLineDialog

Let's look at the Parameters and public Properties first.

  1. We capture added attributes, though we only use class.
  2. Cascade turns on/off parameter cascading of this i.e., the instance of InlineDialog. Default is true.
  3. Transparent sets the background to either transparent or translucent. The demo is set to transluscent so you can see it switch in and out.
  4. ChildContent is the content between <InlineDialog> and </InlineDialog>.
  5. IsLocked is a read only Property for checking the component state.
C#
[Parameter(CaptureUnmatchedValues = true)] 
public IDictionary<string, object> 
  AdditionalAttributes { get; set; } = new Dictionary<string, object>();
[Parameter] public bool Cascade { get; set; } = true;
[Parameter] public bool Transparent { get; set; } = true;
[Parameter] public RenderFragment ChildContent { get; set; }
public bool IsLocked => this._isLocked;

The private properties:

  1. Inject IJSRuntime to give access to the JavaScript Interop and set/unset the browser BeforeUnload event.
  2. CssClass builds the HTML class attribute for the component, combining any entered classes with those built by the component.
  3. The CSS properties define the various CSS options for class.
  4. _isLocked in the private field for controlling lock state.
C#
[Inject] private IJSRuntime _js { get; set; }

private string CssClass => (AdditionalAttributes != null && 
        AdditionalAttributes.TryGetValue("class", out var obj))
    ? $"{this.frontcss}
    { Convert.ToString(obj, CultureInfo.InvariantCulture)}"
    : this.frontcss;

private string backcss = string.Empty;
private string frontcss = string.Empty;
private string _backcss => this.Transparent ? "back-block-transparent" : "back-block";
private string _frontcss => this.Transparent ? "fore-block-transparent" : "fore-block";
private string __backcss => string.Empty;
private string __frontcss => string.Empty;
private bool _isLocked;

There are two public methods: Lock and Unlock. These change the CSS classes. SetPageExitCheck interfaces with the JavaScript functions to add or remove the beforeunload event on Window. The code is shown below:

C#
public void Lock()
{
    this._isLocked = true;
    this.backcss = this._backcss;
    this.frontcss = this._frontcss;
    this.SetPageExitCheck(true);
    this.InvokeAsync(StateHasChanged);
}

public void Unlock()
{
    this._isLocked = false;
    this.backcss = this.__backcss;
    this.frontcss = this.__frontcss;
    this.SetPageExitCheck(false);
    this.InvokeAsync(StateHasChanged);
}

private void SetPageExitCheck(bool action)
    => _js.InvokeAsync<bool>("cecblazor_setEditorExitCheck", action);

The JavaScript in site.js:

JavaScript
window.cecblazor_setEditorExitCheck = function (show) {
    if (show) {
        window.addEventListener("beforeunload", cecblazor_showExitDialog);
    }
    else {
        window.removeEventListener("beforeunload", cecblazor_showExitDialog);
    }
}

window.cecblazor_showExitDialog = function (event) {
    event.preventDefault();
    event.returnValue = "There are unsaved changes on this page.  Do you want to leave?";
}

Moving on to the Razor for the component:

  1. We add a div with the CSS class _backcss: this is either back-block-transparent or back-block when Locked or empty when Unlocked.
  2. We add a div with the Css class _frontcss: this is either fore-block-transparent or fore-block when Locked or empty when Unlocked combined with any class attribute value we have added to the component.
  3. We cascade this if Cascade is true.
Razor
<div class="@this.backcss"></div>

<div class="@this.CssClass">
    @if (this.Cascade)
    {
        <CascadingValue Value="this">
            @this.ChildContent
        </CascadingValue>
    }
    else
    {
        @this.ChildContent
    }
</div>

Moving on to the component CSS, which is where the magic happens. We implement a similar CSS technique to that used in modal dialogs, adding a transparent or translucent layer over the page content to block content below the layer, and place the contents of InlineDialog in front of that layer. If you use a lot on z-index layers in your application, you may need to tweak the Z-index to make sure it sits on top.

CSS
div.back-block {
    display: block;
    position: fixed;
    z-index: 1; /* Sit on top */
    left: 0;
    top: 0;
    width: 100; /* Full width */
    height: 100; /* Full height */
    overflow: auto; /* Enable scroll if needed */
    background-color: RGBA(224, 224, 224, 0.4);/* the translucent effect*/
}

div.back-block-transparent {
    display: block;
    position: fixed;
    z-index: 1;          /* Sit on top */
    left: 0;
    top: 0;
    width: 100%;    /* Full width */
    height: 100%;   /* Full height */
    overflow: auto;  * Enable scroll if needed */
    background-color: transparent; 
}

div.fore-block-transparent {
    display: block;
    position: relative;
    z-index: 2;      /* Sit on top */
}

div.fore-block {
    display: block;
    position: relative;
    z-index: 2; /* Sit on top */
    background-color: RGB(255, 255, 255);/* need to set the colour, adjust as necessary */
}

Wrap Up

This solution uses the same techniques used by modal dialogs in placing a barrier between the controls on the page and the contents of the control. It's an in place modal dialog. Lock inserts the barrier and Unlock removes it. We add the JavaScript Interop to turn on add off the beforeunload event on the browser. Choose between a transparent or transluscent layer, or code your own CSS.

Having developed many solutions to solve this problem, and written articles about them, I'm a little flabbergasted to finally find a solution that's this easy. The best solutions are always the simplest!

If you are reading this well into the future, the most recent version of this article will be here.

History

  • 17th March, 2021: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Retired Cold Elm
United Kingdom United Kingdom
Ex Geologist, Project Manager, Web Hoster, Business Owner and IT Consultant. Now, a traveller to places less travelled. And part time developer trying to keep up!

If you feel like saying thanks, the next time you see a charity request, DONATE. No matter how small, no matter who, it'll count. If you have a choice in the UK, Barnados.

Comments and Discussions

 
GeneralMy vote of 4 Pin
mag1322-Mar-21 4:38
mag1322-Mar-21 4:38 
GeneralRe: My vote of 4 Pin
Shaun C Curtis22-Mar-21 9:06
mvaShaun C Curtis22-Mar-21 9:06 
First line of Links Section - not as obvious as it should be, was edited Thumbs Up | :thumbsup: GitHub - ShaunCurtis/Blazor.Database[^] Will update article to make more obvious. Ta
Questionsource ? Pin
mag1322-Mar-21 4:37
mag1322-Mar-21 4:37 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.