Last active
November 28, 2024 09:37
-
-
Save teneko/24a62f81eedabeb2faf7884efdd44663 to your computer and use it in GitHub Desktop.
C# Blazor Base Component or Super Component as High-Order-Component or Higher-Level-Component
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Microsoft.AspNetCore.Components; | |
using Microsoft.AspNetCore.Components.Rendering; | |
/// <summary> | |
/// A helper to implement the concept of a high-order component. | |
/// Imagine you have a multiple concrete page content components but now you want to wrap each of them with the SAME frame. | |
/// The current blazor workflow would be to manually wrap every concrete page content component by the page frame component. | |
/// Wouldn't it be cool if every concrete page content component would inherit from page frame component and wrap its | |
/// derived component with the desired page frame? By declaring HighOrderComponent in the base component and using it | |
/// in the parent component you can use the concept of a high-order component. | |
/// </summary> | |
/// <param name="manuallyAllowSubComponentRendering"> | |
/// Enable it for deubg pruposes. | |
/// Call <see cref="AllowSuperFragmentRendering" /> in <see cref="ComponentBase.OnAfterRender(bool)"/> by yourself. | |
/// </param> | |
public class HighOrderComponent(bool manuallyAllowSubComponentRendering = false) | |
{ | |
private RenderFragment? _self; | |
private RenderFragment? _sub; | |
private bool _hasSelfComponentBeenSetExplictly; | |
private bool _allowSubComponentRendering = false; | |
public RenderFragment Self { | |
get => _self ?? throw new InvalidOperationException("HOC is in invalid state: no self component has been set."); | |
set { | |
if (value is null) { | |
throw new ArgumentNullException("Self component must not be null"); | |
} | |
if (_self is not null) { | |
throw new InvalidOperationException("Self component must be initialized once."); | |
} | |
_self = value; | |
_hasSelfComponentBeenSetExplictly = true; | |
} | |
} | |
public bool BuildRenderTree(RenderFragment sub, RenderFragment super, RenderTreeBuilder builder) | |
{ | |
ArgumentNullException.ThrowIfNull(sub); | |
ArgumentNullException.ThrowIfNull(super); | |
ArgumentNullException.ThrowIfNull(builder); | |
if (sub == super) { | |
throw new ArgumentException("Expected the owning component's render fragment to be derived from the base component's render fragment, so they do not result into reference-equality."); | |
} | |
if (_sub is null) { | |
_sub = sub; | |
} else if (!_hasSelfComponentBeenSetExplictly && _sub != sub) { | |
throw new InvalidOperationException("Super component render fragment reference identity changed."); | |
} | |
if (_allowSubComponentRendering) { | |
return true; | |
} | |
_allowSubComponentRendering = true; | |
super(builder); | |
if (!manuallyAllowSubComponentRendering) { | |
_allowSubComponentRendering = false; | |
} | |
return false; | |
} | |
public void AllowSuperFragmentRendering() | |
{ | |
if (!manuallyAllowSubComponentRendering) { | |
throw new InvalidOperationException($"{nameof(HighOrderComponent)} was not configured to allow super fragment rendering manually."); | |
} | |
_allowSubComponentRendering = true; | |
} | |
public static implicit operator RenderFragment(HighOrderComponent hoc) => hoc._sub ?? throw new InvalidOperationException("HOC is in invalid state: no sub component has been set."); | |
} |
Problems I faced with this approach: When using a base component as HOC, than you must use ::deep to also target the derived classes and you should not use components providing cascading value(s) (e.g. EditForm) inside HOCs (in derived components should be okay, but not if the derived components is a HOC for another derived component), because it leads to a render loop. I couldn't find out a solution to it yet.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Example how to chain two classes:
Template.razor
Template2.razor
Example how to chain three and more classes
Template.razor
Template2.razor
Template3.razor