Skip to content

Blazor handling of overlay components #54768

@Kebechet

Description

@Kebechet

Summary

Blazor guidelines discourage users from creating Blazor components from .cs files and at the same time they show warning BL0005: Component parameter should not be set outside of its component. I understand that it is by design.

This behavior is okay for most cases. Until you start to work with overlay components. For example Popups or BottomSheets then it becomes quite PITA to follow this recommendation.

Motivation and goals

Reconsider or change behavior of this warnings/guidelines so that work with similar type of components is much easier.

Current state (3 options how to create popup component)

  1. Create the component on the page where do you use it. So in .razor you create element <Popup Arg1="" arg2="" /> and you either reference it by @ref or trigger it's functionality by some special service
  2. Create the component from the function in .razor @code{} section. Because only in .razor we can use @<div></div> as the input parameter
  3. Create a service with hardcoded parameters. After calling this service it will fill the component somewhere

Disadvantages of these current approaches:

  • code placement inconsitancy - we split all UI items into .razor, we dont use @code and all functionality of the UI we extract to .razor.cs. But in .razor.cs we cant use the @<div></div>
  • control flow - we would have to place some code responsible for filling the component arguments to @code part. And then call it from .razor.cs
  • usually we put a little bit of C# code to blazor elements and not the other way around. So using special syntax like @<div></div> as the method parameter is really ugly

it will be better showcased in EXAMPLES section

In scope / Out of scope

  • I really dont know how this should be handled properly

Risks / unknowns

dunno

Examples

Current state in existing component libraries:

Radzen

  • @<div></div> as parameter in .razor @code section
@code {
    async Task ShowDialogWithCustomCssClasses()
    {
        await DialogService.OpenAsync("Dialog with custom CSS classes", ds =>
        @<div>
            This dialog has custom CSS classes.
        </div>, new DialogOptions() {
                    CssClass = "custom-dialog-class",
                    WrapperCssClass = "custom-dialog-wrapper-class"
                });
    }
}
  • hardcoded arguments - required to create some SideDialogOptions for each type of dialog component
await DialogService.OpenSideAsync<DialogSideContent>(
    "Side Panel", 
    options: new SideDialogOptions {     
        CloseDialogOnOverlayClick = closeDialogOnOverlayClick, 
        Position = position, 
        ShowMask = showMask 
    });
}

MudBlazor

  • creating component in the .razor and then calling it's methods from service
<MudDialog>
    <DialogContent>
        Dialog Content
    </DialogContent>
    <DialogActions>
        <MudButton OnClick="Cancel">Cancel</MudButton>
        <MudButton Color="Color.Primary" OnClick="Submit">Ok</MudButton>
    </DialogActions>
</MudDialog>
@code {
    [CascadingParameter] MudDialogInstance MudDialog { get; set; }

    void Submit() => MudDialog.Close(DialogResult.Ok(true));
    void Cancel() => MudDialog.Cancel();
}

Our approach:

  • wherever we inject popup service we can create the component
  • for this to work we use reflection to create RenderFragment from already created component provided as input argument.
  • in our case the Input (QuestionPopupComponent) is a template component. So if we want to show different popup, we create another template that implements IPopupReturnable<TOutput> or in case of BottomSheets IBottomSheetReturnable<TOutput>
  • input arguments are always hardcoded. Inside the popup components we inject In-memory state container service that persists data locally.
//this way of creating the popup is really nice for usage
var isSuccess =  await _popupService.Show(
    new QuestionPopupComponent{
        Title = "Are you sure?",
        Message = "Do you want to continue?",
        ConfirmButtonText = "Yes",
        CancelButtonText = "No"
    }
);

if (isSuccess is null)
{
    // user closed the popup
} 
else if(isSuccess == true)
{
    // user clicked the button that returns true
} 
else if(isSuccess == false){
    // user clicked the button that returns false
}

Discussion

a) Are any of my claims/assumptions incorrect ?
b) What do you think about this way of creating popups, bottom-sheets and other components that overlay the original UI layer ?
c) Is it possible to do this even without reflection ? If RenderFragment builder OpenComponent could accept IComponent or ComponentBase as an argument I could get rid of whole reflection logic.
d) Are there any other ways how such components can be created except those examples I provided ?

Metadata

Metadata

Labels

area-blazorIncludes: Blazor, Razor Components

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions