From the official Microsoft documentation:
Component parameters should be declared as auto-properties, meaning that they shouldn't contain custom logic in their get or set accessors. For example, the following StartData property is an auto-property:
[Parameter] public DateTime StartData { get; set; }
https://learn.microsoft.com/en-us/aspnet/core/blazor/components/?view=aspnetcore-9.0
Well, this is the dogma because I don't see the justification.
I agree that there must not be the complicated logic, but there is the important case when the simple logic must be - it is the validation.
The value of theme
parameter of AdmonitionBlock
component must be either the element of StandardThemes
enumeration or the element of CustomThemes
enumeration which must be defined via defineThemes
method before use of AdmonitionBlock
component:
public partial class AdmonitionBlock : Microsoft.AspNetCore.Components.ComponentBase
{
public static string CSS_NAMESPACE = "AdmonitionBlock";
public enum StandardThemes { regular }
protected internal static Type? CustomThemes;
public static void defineThemes(Type CustomThemes)
{
YDF_ComponentsHelper.ValidateCustomTheme(CustomThemes);
AdmonitionBlock.CustomThemes = CustomThemes;
}
protected string _theme = AdmonitionBlock.StandardThemes.regular.ToString();
[Microsoft.AspNetCore.Components.Parameter]
public object theme
{
get => this._theme;
set => YDF_ComponentsHelper.
AssignThemeIfItIsValid<AdmonitionBlock.StandardThemes>(value, AdmonitionBlock.CustomThemes, ref this._theme);
}
protected internal static bool mustConsiderThemesCSS_ClassesAsCommon = YDF_ComponentsHelper.areThemesCSS_ClassesCommon;
public static void considerThemesAsCommon()
{
AdmonitionBlock.mustConsiderThemesCSS_ClassesAsCommon = true;
}
[Microsoft.AspNetCore.Components.Parameter]
public bool areThemesCSS_ClassesCommon { get; set; } =
YDF_ComponentsHelper.areThemesCSS_ClassesCommon || AdmonitionBlock.mustConsiderThemesCSS_ClassesAsCommon;
}
The validation is being executed by the ComponentsHelper
class (not only for AdmonitionBlock
component):
public abstract class ComponentsHelper
{
public static bool areThemesCSS_ClassesCommon = false;
public static void ValidateCustomTheme(Type CustomThemes)
{
if (!CustomThemes.IsEnum)
{
throw new CustomYDF_ThemeIsNotEnumerationException();
}
}
public static void AssignThemeIfItIsValid<TStandardThemes>(object value, Type? customThemes, ref string _theme)
{
if (value is TStandardThemes standardTheme)
{
_theme = $"{ standardTheme }";
return;
}
string stringifiedThemeValue = value.ToString() ?? "";
if (customThemes is not null)
{
Type customThemesType = customThemes;
if (customThemesType.IsEnum && Enum.GetNames(customThemesType).Contains(stringifiedThemeValue))
{
_theme = stringifiedThemeValue;
return;
}
}
throw new InvalidThemeParameterForYDF_ComponentException();
}
}
Following the advices, turned theme
to auto-property and added the SetParametersAsync
method:
public partial class AdmonitionBlock : Microsoft.AspNetCore.Components.ComponentBase
{
public enum StandardThemes { regular }
protected internal static Type? CustomThemes;
public static void defineThemes(Type CustomThemes)
{
ComponentsHelper.ValidateCustomTheme(CustomThemes);
AdmonitionBlock.CustomThemes = CustomThemes;
}
[Microsoft.AspNetCore.Components.Parameter]
public object theme { get; set; } = AdmonitionBlock.StandardThemes.regular;
public string themeName => this.theme.ToString() ?? "";
public override Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters)
{
parameters.SetParameterProperties(this);
if (parameters.TryGetValue<object>(nameof(this.theme), out object? value))
{
ComponentsHelper.ValidateTheme<AdmonitionBlock.StandardThemes>(value, AdmonitionBlock.CustomThemes);
}
return base.SetParametersAsync(ParameterView.Empty);
}
}
where ValidateTheme
is:
public abstract class ComponentsHelper
{
public static void ValidateTheme<TStandardThemes>(object? value, Type? customThemes)
{
if (value is TStandardThemes standardTheme)
{
return;
}
string stringifiedThemeValue = value?.ToString() ?? "";
if (customThemes is not null)
{
Type customThemesType = customThemes;
if (customThemesType.IsEnum && Enum.GetNames(customThemesType).Contains(stringifiedThemeValue))
{
return;
}
}
throw new InvalidThemeParameterForYDF_ComponentException();
}
}
Unfortunately, I have not understood will SetParametersAsync
called on change of any parameter or not, however looks like it will be. If so, the above logic is not equivalent to the first example, because the validation of all parameters will be invoked on any parameters has changes that it the performance impact.
I have to politely disagree, though it's a reasonable initial conclusion to draw when starting out in Blazor. Components are far more complex than initial impressions.
It isn't dogma, as explained in https://github.com/dotnet/aspnetcore/issues/26230, Jon Skeet's comment, and other sources. It's sound advice, because, in almost all circumstances, people don't understand the consequences of what they're doing. There have been many questions on here on the topic.
Parameters get applied to the component in SetParametersAsync
. If you want to apply setting logic, apply it here. It can be sync or async and it will run before any component rendering.
Here's the basic override template:
public override Task SetParametersAsync(ParameterView parameters)
{
// First apply the incoming parameters to the component properties
parameters.SetParameterProperties(this);
// put your logic here such as detecting the change in a parameter
// from a cached local copy
// Call base to run the rest of the OnInitialized and SetParameters methods
return base.SetParametersAsync(ParameterView.Empty);
}
You can implement your own setting logic if you wish as detailed here https://learn.microsoft.com/en-us/aspnet/core/blazor/performance?view=aspnetcore-9.0#implement-setparametersasync-manually
This now looks like you're trying to fit a round peg in a square hole.
You're passing an object into the component and trying to validate if it's a valid enum
value. If so, you set a string value [maybe used in Css?].
Theme suggests you will be using this in more than one place. In which case you can move everything out of the component and into a separate object like this.
This implements the Notifcation pattern using an event. I've sinmplified the validation code. Not sure why you would want to raise an exception if the value is not valid.
public class ComponentThemeProvider
{
public string Theme { get; private set; } = StandardThemes.Regular.ToString();
public event EventHandler<string>? ThemeChanged;
public bool SetTheme<TTheme>(TTheme value, object? senderToken = null)
{
if (value is not null && typeof(TTheme).IsEnum)
{
this.Theme = $"{value.ToString()}";
this.ThemeChanged?.Invoke(senderToken, this.Theme);
return true;
}
return false;
}
}
A couple of enums
to test with:
public enum StandardThemes { Regular }
public enum CustomThemes { Dark, Light }
Depending on the scope you can either register the provider as a Scoped Service or cascade it in a component tree.
Here's a demo cascading the provider in a component tree.
The ComponentThemeProvider
is cascaded and is a fixed. Channges are communicated through the event.
@page "/"
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<div class="m-2">
<button class="btn btn-dark" @onclick="this.SetCustomThme">Dark Theme</button>
<button class="btn btn-primary" @onclick="this.ResetTheme">Reset Theme</button>
</div>
<CascadingValue IsFixed Value="this._themeProvider">
<ThemeComponent>
<div>Hello</div>
<ThemeComponent>
<div>Hello 2</div>
</ThemeComponent>
</ThemeComponent>
</CascadingValue>
@code {
private readonly ComponentThemeProvider? _themeProvider = new();
private void SetCustomThme()
{
_themeProvider?.SetTheme<CustomThemes>(CustomThemes.Dark);
}
private void ResetTheme()
{
_themeProvider?.SetTheme<StandardThemes>(StandardThemes.Regular);
}
}
And the component:
@implements IDisposable
<h3>Theme Component : @this.ThemeProvider.Theme</h3>
@this.ChildContent
@code {
[CascadingParameter] private ComponentThemeProvider ThemeProvider { get; set; } = new();
[Parameter] public RenderFragment? ChildContent { get; set; }
protected override void OnInitialized()
{
this.ThemeProvider.ThemeChanged += this.OnThemeChanged;
}
private void OnThemeChanged(object? sender, string theme)
{
this.StateHasChanged();
}
public void Dispose()
{
this.ThemeProvider.ThemeChanged -= this.OnThemeChanged;
}
}