Von MVC UIHint zu Blazor Komponenten

Im letzten Beitrag ging ich auf die DataAnnotations und die Unterstützung in Blazor ein. In ASP.NET MVC existiert ein weiteres Attribut UIHint.

Was sind UIHints?

In der Welt des klassischen ASP .NET MVC sind die UIHints eine grosse Unterstützung für den Aufbau einheitlicher Komponenten im Frontend. In ASP .NET Core und .NET 5 wird dieses Konzept nach wie vor unterstützt. Die dafür notwendigen Informationen werden direkt auf einem Property eines ViewModels gesetzt: 

[UIHint("Text")] 
[Display(Name="Nachname")] 
[Required(ErrorMessage = "Bitte Nachnamen eingeben.")] 
public string Name { get; set; } 

Was sagt aber diese UIHint «Text» aus? Dieser «Text» kann jetzt als Textbox unter den EditorTemplates aufgebaut werden. Diese befinden sich an folgendem Ort: 

Die Abbildung zeigt den Ablageort der Template zum Ordnerpfad Views(Shared/EditorTemplates

Mithilfe dem «@model» kann auf den Wert des Properties zugegriffen werden. Durch die HTML-Helpers kann die Textbox mit Label und Validation-Message aufgebaut werden: 

 @model object 

<div> 
    @Html.LabelFor(x => x) 
    <br /> 
    @Html.TextBoxFor(x => x) 
    <br />
    @Html.ValidationMessageFor(x => x) 
    <br /> 
</div> 

Soll für das Property in einem MVC-Form die UIHint-Komponente angezeigt werden, geht das wie folgt: 

@Html.EditorFor(a => a.Name) 

Somit ist zu erkennen, dass die Art der Elemente ausschliesslich über das ViewModel gesteuert werden können. Das gleiche Prinzip gibt es auch mit «DisplayTemplates» und «@Html.DisplayFor(a => a.Name)». Damit können generelle Templates für das Lesen eines ViewModels verwendet werden.  

Blazor und UIHints?

Nach ausführlicher Recherche gibt es bis anhin keinen ähnlichen Ansatz für Blazor. Der grosse Unterschied besteht bei den HTML-Helpers, welche im Blazor nicht existieren. Aus diesem Grund entfallen gewisse Möglichkeiten, wie zum Beispiel «LabelFor», das automatisch das Display-Attribute auf einem Property beachtet. Wichtig zu erwähnen ist, dass dieses Attribute gleichwohl im Blazor angewendet werden kann. Es gibt jedoch bis jetzt keine vorgefertigte Lösung von Microsoft für die Anwendung des Display-Attributes in vereinfachter Form.  

Was heisst das jetzt für Blazor?

Bleiben wir bei diesem Display-Attribute. Ich habe ein Beispiel erarbeitet, in welchem ich aufzeigen will, dass gleiche Möglichkeiten schnell im Blazor geschaffen werden können. Grundsätzlich lassen sich im Blazor wiederverwendbare Elemente mit Razor-Components realisieren. Diese unterscheiden sich nur minimal von den Blazor-Pages. Bei solchen Komponenten handelt es sich genau um die gleiche Dateiendung und genau dem gleichen Prinzip, nur braucht es keine Route in der «.razor»-Datei («@page..» nicht notwendig). Genau diese Möglichkeit habe ich für eine Textbox mit Beachtung vom Display-Attribute angewendet. Der daraus entstehende Komponente «TextEditor.razor» sieht wie folgt aus: 

@using System.Linq.Expressions; 

@typeparam T 

<div> 
    <label for="@Id">@DisplayName</label> 
    <br /> 
    <input type="text" id="@Id" @bind="@Value" /> 
    <br /> 
    <ValidationMessage For="@For" /> 
    <br /> 
</div> 

@code { 
    private T _value; 

    [Parameter] 
    public T Value 
    {
        get => _value; 
        set 
        { 
            if(Equals(value, _value)) { 
                return; 
            } 
            _value = value; 
            ValueChanged.InvokeAsync(value); 
        } 
    } 
     
    [Parameter] 
    public EventCallback<T> ValueChanged { get; set; } 

    [Parameter] 
    public Expression<Func<T>> For { get; set; } 

    [Parameter] 
    public string Id { get; set; } 

    public string DisplayName => For.GetDisplayName(); 

    protected override void OnInitialized() 
    { 
        if (string.IsNullOrEmpty(Id)) 
        { 
            Id = DisplayName.Replace(" ", string.Empty); 
        }
    } 
} 

Für die Anwendung des Display-Attributes existiert eine ganz einfache Extensions-Methode: 

public static class ModelExtensions 
{ 
    public static string GetDisplayName<T>(this Expression<Func<T>> expr) 
    { 
        var expression = (MemberExpression)expr.Body; 
        var value = expression.Member.GetCustomAttribute(typeof(DisplayAttribute)) as DisplayAttribute; 
        return value?.Name ?? expression.Member.Name ?? ""; 
    } 
} 

Schon haben wir eine Komponente, die wie folgt eingesetzt werden kann: 

<TextEditor For="(() => Model.Name)" @bind-Value="@Model.Name" /> 

Die Möglichkeit, ein UIHint auf dem Model zu setzen, ist im Blazor nicht vorhanden. In verschiedenen Diskussionen auf GitHub wird auch explizit erwähnt, dass eine äquivalente Implementation vom UIHint nicht geplant ist. Der Fokus soll stark auf die Razor-Komponenten gesetzt werden. Dieser Grundsatz lässt sich auch in verschiedensten Blazor UI-Libraries feststellen. Als Beispiel nehmen wir die Library «MatBlazor», indem die verschiedenen Komponenten wie folgt aufgebaut werden: 

<p> 
    <MatTextField Label="Comment" @bind -Value="myModel.Comment" /> 
    <ValidationMessage For="@(() => myModel.Comment)" /> 
</p> 
<p> 
    <MatDatePicker Label="Start Date" @bind -Value="myModel.StartDate" /> 
    <ValidationMessage For="@(() => myModel.StartDate)" /> 
</p> 
<p> 
    <MatDatePicker Label="eND Date" @bind -Value="myModel.EndDate" /> 
    <ValidationMessage For="@(() => myModel.EndDate)" /> 
</p> 
<p> 
    <MatSelect Label="Select" @bind -Value="myModel.Gender"> 
        <MatOptionString Value=""></MatOptionString> 
        <MatOptionString Value="M">M</MatOptionString> 
        <MatOptionString Value="W">W</MatOptionString> 
    </MatSelect> 
    <ValidationMessage For="@(() => myModel.Gender)" /> 
</p>  

Dieses Beispiel ist auf der offiziellen Demo-Seite von «MatBlazor». Sowohl Label, als auch die Validation-Message müssen aber explizit angegeben werden. Natürlich ist es jedem frei, diese wiederholenden Elemente in eine Komponente zu verlagern (siehe vorheriges Beispiel «TextEditor.razor»). Daher ist es im Blazor nur eine Frage des initialen Aufwandes. Möchte man sich gewisse Ansätze vereinfachen, können diese in Form von Helper-Klassen oder Komponenten erarbeitet werden.