Eine relativ einfache Validierung von Eingabemasken kann bereits mit den Bordmitteln von .NET durch das Hinzufügen von ValidationAttribute
-Dekorationen implementiert werden. Schnell stößt man jedoch an Grenzen, wodurch aufwändige Anpassungen notwendig werden, etwa die Implementierung eigener ValidationAttribute
. Ein einfaches Beispiel hierfür wäre die Validierung zweier Datumsfelder, bei der der Wert im zweiten Feld stets größer als der im ersten sein muss. Solch eine Regel ließe sich noch relativ unkompliziert umsetzen.
Doch wie gestaltet sich die Validierung, wenn plötzlich fünf oder mehr Felder mit wechselnden Abhängigkeiten und Regeln überprüft werden müssen? Für solche Szenarien gibt es einige NuGet-Pakete, die diese Anforderungen bereits effizient lösen können. Vorausgesetzt, diese Pakete sind sowohl client- als auch serverseitig in Blazor anwendbar. Im Folgenden werden zwei dieser Pakete näher betrachtet und ihre Vor- und Nachteile gegenübergestellt. Ziel ist es, eine Entscheidungshilfe bereitzustellen, welches Paket sich für spezifische Problemstellungen am besten eignet.
Es empfiehlt sich, die Validierungsregeln pro Datenklasse in einer separaten Klasse zu definieren, um die Struktur zu optimieren. Ein Beispiel für die Validierung eines Zahlenfelds RangeStart
in Abhängigkeit von drei weiteren Feldern (Fixed
, NumberLength
und RangeEnd
) wird nachfolgend erläutert. Das Feld NumberLength
bestimmt dabei dynamisch den Zahlenbereich, in dem sich RangeStart
bewegen darf. Beispielsweise gilt bei NumberLength = 4
ein Bereich von 1000 bis 9999. Zusätzlich muss RangeStart
kleiner als RangeEnd
sein. Diese Regeln gelten jedoch nur, wenn Fixed
nicht gesetzt ist. Die Konfiguration ist dabei unkompliziert.
public class DemoDTOValidator : AbstractValidator<DemoDTO>
{
public DemoDTOValidator()
{
...
RuleFor(dto => dto.RangeStart)
.NotEmpty()
.When(dto => !dto.Fixed, applyConditionTo: ApplyConditionTo.AllValidators)
.WithMessage("Zahlenbereich (von) muss angegeben werden")
.Must((dto, rangeStart) => IntWithVariableLength(rangeStart, dto.NumberLength))
.When(dto => !dto.Fixed, applyConditionTo: ApplyConditionTo.AllValidators)
.WithMessage(dto => $"Die eingegebene Zahl muss eine Länge von {dto.NumberLength} haben")
.LessThan(dto => dto.RangeEnd)
.When(dto => !dto.Fixed, applyConditionTo: ApplyConditionTo.AllValidators)
.WithMessage("Zahlenbereich (von) muss kleiner sein als 'bis'");
...
}
private bool IntWithVariableLength(int? value, int? length)
{
return value.HasValue && length.HasValue && value.Value.ToString().Length == length.Value;
}
}
Der Validator selbst kann eine beliebige Methode sein (hier: IntWithVariableLength
). Diese Methode ist lediglich eine von vielen Regeln, die im Konstruktor der Validator-Klasse definiert werden können. Entsprechend müssen oder können auch Regeln für RangeEnd
definiert werden.
Validation-Attribute müssen nicht zwingend in der Datenklasse definiert sein, können jedoch ergänzend zur FluentValidation genutzt werden, beispielsweise für grundlegende Überprüfungen wie Required
oder Range
.
public class DemoDTO
{
public int Id { get; set; }
[Required(ErrorMessage = "Bitte geben Sie einen Wert ein")]
[Range(4, 8, ErrorMessage = "{0} muss einen Wert zwischen {1} und {2} haben")]
[Display(Name = "Zahlenlänge")]
public int? NumberLength { get; set; }
...
public bool Fixed { get; set; }
public string? FixedValue { get; set; }
public int? RangeStart { get; set; }
public int? RangeEnd { get; set; }
}
Versionen und Lizenz
In der Program.cs
muss beim App-Start zunächst Blazorise registriert und konfiguriert werden. Die Option Immediate
ermöglicht eine Validierung bei jeder Tastatureingabe, anstatt erst beim Verlassen des Eingabefelds. Darüber hinaus muss die FluentValidation dem Blazorise-Service hinzugefügt werden.
public class Program
{
public static void Main(string[] args)
{
...
builder.Services
.AddBlazorise(options => options.Immediate = true;)
.AddBlazoriseFluentValidation();
...
builder.Services
.AddValidatorsFromAssemblyContaining<DemoDTOValidator>();
...
var host = builder.Build();
...
host.Run();
}
}
Zusätzlich ist es notwendig, die Validierungsregeln, beispielsweise über ein Assembly, hinzuzufügen.
Die Definition der Razor-Page erfolgt wie folgt:
<EditForm EditContext="EditContext">
<DataAnnotationsValidator />
<Validations @ref="@fluentValidations" Mode="ValidationMode.Auto"
EditContext="EditContext" HandlerType="typeof(FluentValidationHandler)">
<Validation>
<Field>
<Label>Zahlenlänge (DataAnnotation)</Label>
<NumericEdit Placeholder="Von" TValue="int?" @bind-Value="Item.NumberLength">
<Feedback>
<ValidationError />
</Feedback>
</NumericEdit>
</Field>
</Validation>
<Validation>
<Field>
<Label>Zahlenbereich</Label>
<NumericEdit Placeholder="Von" TValue="int?" @bind-Value="Item.RangeStart">
<Feedback>
<ValidationError />
</Feedback>
</NumericEdit>
</Field>
</Validation>
<Validation>
<Field>
<NumericEdit" Placeholder="Bis" TValue="int?" @bind-Value="Item.RangeEnd>
<Feedback>
<ValidationError />
</Feedback>
</NumericEdit>
</Field>
</Validation>
</Validations>
<Button Color="Color.Primary" Clicked="@OnSave" Disabled="HasErrors">Save</Button>
</EditForm>
Code-Behind:
public partial class BlazoriseFluentValidation : ComponentBase
{
public EditContext EditContext { get; set; }
public DemoDTO Item { get; set; }
public Validations fluentValidations;
bool HasErrors { get; set; } = true;
protected override async Task OnParametersSetAsync()
{
Item ??= new() { NumberLength = 5 };
EditContext = new EditContext(Item);
EditContext.OnFieldChanged += EditContext_OnFieldChanged;
}
private void EditContext_OnFieldChanged(object? sender, FieldChangedEventArgs e)
{
bool isValid1 = EditContext.Validate(); // erfordert <DataAnnotationsValidator />
bool isValid = fluentValidations.ValidateAll().Result; // erfordert <Validations @ref="@fluentValidations"
HasErrors = !(isValid && isValid1);
}
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
}
Damit funktioniert die Validierung. Einziger Nachteil: Die Lizenz des Pakets ist nicht für kommerzielle Nutzung geeignet.
Versionen und Lizenz
Dieses Paket basiert ebenfalls auf FluentValidation (Version 11.9.1) und erlaubt dadurch eine einheitliche Definition der Validierungsregeln.
In der Program.cs
ist keine weitere Konfiguration notwendig.
In der Razor-Page befinden sich mehrere Eingabefelder. Für die Validierung relevant sind jedoch nur drei Felder: NumberLength
, RangeStart
und RangeEnd
. Diese Felder sind in der Validierung voneinander abhängig.
Hier das Resultat einer entsprechenden Validierung:
Die Definition der Razor-Page enthält eine EditForm
mit den drei Controls, zugehörigen Labels und einer ValidationMessage
. Neben dem bekannten DataAnnotationsValidator
wird der FluentValidationValidator
integriert.
<EditForm EditContext="EditContext">
<DataAnnotationsValidator />
<FluentValidationValidator />
<div class="field">
<label for="@Item.NumberLength">Zahlenlänge (DataAnnotation)</label>
<InputNumber class="d-block" @bind-Value="Item.NumberLength" />
<ValidationMessage TValue="int?" For="() => Item.NumberLength"></ValidationMessage>
</div>
<div class="field">
<label for="@Item.RangeStart">Zahlenbereich</label>
<InputNumber class="d-block" @bind-Value="Item.RangeStart" placeholder="Von" />
<ValidationMessage For="() => Item.RangeStart"></ValidationMessage>
</div>
<div>
<InputNumber class="d-block" @bind-Value="Item.RangeEnd" placeholder="Bis" />
<ValidationMessage TValue="int?" For="() => Item.RangeEnd"></ValidationMessage>
</div>
<button type="button" class="btn btn-primary" onclick="@OnSave" disabled="@HasErrors">Save</button>
</EditForm>
Im Code-Behind wird der EditContext
um einen EventHandler erweitert, der beim Verlassen eines Feldes den gesamten Kontext validiert.
public partial class BlazoredFluentValidation
{
public EditContext? EditContext { get; set; }
public DemoDTO Item { get; set; }
protected override async Task OnInitializedAsync()
{
Item ??= new() { NumberLength = 5 };
EditContext = new EditContext(Item);
EditContext.OnFieldChanged += EditContext_OnFieldChanged;
await base.OnInitializedAsync();
}
protected override async Task OnParametersSetAsync()
{
await base.OnParametersSetAsync();
}
private void EditContext_OnFieldChanged(object? sender, FieldChangedEventArgs e)
{
bool isValid1 = EditContext.Validate(); // erfordert <DataAnnotationsValidator />
HasErrors = !(isValid1);
}
protected void OnSave()
{
}
public void Dispose()
{
if (EditContext is not null)
{
EditContext.OnFieldChanged -= EditContext_OnFieldChanged;
}
}
}
Damit ist die Einrichtung abgeschlossen. Der Aufwand für dieses Paket ist deutlich geringer als für Blazorise.FluentValidation.