Date validator in Blazor Server Side - c#

I have a simple input model for my blazor server side component. I want to use the build in validation for two DateTime properties.
[DataType(DataType.Date)]
public DateTime? FromDate { get; set; }
[DataType(DataType.Date)]
public DateTime? ToDate { get; set; }
How can I only accept ToDate > FromDate?

Solution using custom ValidationAttributes:
DateMustBeAfterAttribute.cs:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class DateMustBeAfterAttribute : ValidationAttribute
{
public DateMustBeAfterAttribute(string targetPropertyName)
=> TargetPropertyName = targetPropertyName;
public string TargetPropertyName { get; }
public string GetErrorMessage(string propertyName) =>
$"'{propertyName}' must be after '{TargetPropertyName}'.";
protected override ValidationResult? IsValid(
object? value, ValidationContext validationContext)
{
var targetValue = validationContext.ObjectInstance
.GetType()
.GetProperty(TargetPropertyName)
?.GetValue(validationContext.ObjectInstance, null);
if ((DateTime?)value < (DateTime?)targetValue)
{
var propertyName = validationContext.MemberName ?? string.Empty;
return new ValidationResult(GetErrorMessage(propertyName), new[] { propertyName });
}
return ValidationResult.Success;
}
}
DateMustBeBeforeAttribute.cs:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class DateMustBeBeforeAttribute : ValidationAttribute
{
public DateMustBeBeforeAttribute(string targetPropertyName)
=> TargetPropertyName = targetPropertyName;
public string TargetPropertyName { get; }
public string GetErrorMessage(string propertyName) =>
$"'{propertyName}' must be before '{TargetPropertyName}'.";
protected override ValidationResult? IsValid(
object? value, ValidationContext validationContext)
{
var targetValue = validationContext.ObjectInstance
.GetType()
.GetProperty(TargetPropertyName)
?.GetValue(validationContext.ObjectInstance, null);
if ((DateTime?)value > (DateTime?)targetValue)
{
var propertyName = validationContext.MemberName ?? string.Empty;
return new ValidationResult(GetErrorMessage(propertyName), new[] { propertyName });
}
return ValidationResult.Success;
}
}
Usage:
public class DateTimeModel
{
[Required]
[DateMustBeBefore(nameof(ToDate))]
[DataType(DataType.Date)]
public DateTime? FromDate { get; set; }
[Required]
[DateMustBeAfter(nameof(FromDate))]
[DataType(DataType.Date)]
public DateTime? ToDate { get; set; }
}
The fields are linked so we need to notify EditContext when any one of them changes to re-validate the other.
Example EditForm:
<EditForm EditContext="editContext" OnInvalidSubmit="#HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<p>
<label>
From Date:
<InputDate TValue="DateTime?"
Value="dateTimeModel.FromDate"
ValueChanged="HandleFromDateChanged"
ValueExpression="() => dateTimeModel.FromDate" />
</label>
<ValidationMessage For="#(() => dateTimeModel.FromDate)" />
</p>
<p>
<label>
To Date:
<InputDate TValue="DateTime?"
Value="dateTimeModel.ToDate"
ValueChanged="HandleToDateChanged"
ValueExpression="() => dateTimeModel.ToDate" />
</label>
<ValidationMessage For="#(() => dateTimeModel.ToDate)" />
</p>
<button type="submit">Submit</button>
</EditForm>
#code {
private EditContext? editContext;
private DateTimeModel dateTimeModel = new();
protected override void OnInitialized()
{
editContext = new EditContext(dateTimeModel);
}
private void HandleFromDateChanged(DateTime? fromDate)
{
dateTimeModel.FromDate = fromDate;
if (editContext != null && dateTimeModel.ToDate != null)
{
FieldIdentifier toDateField = editContext.Field(nameof(DateTimeModel.ToDate));
editContext.NotifyFieldChanged(toDateField);
}
}
private void HandleToDateChanged(DateTime? toDate)
{
dateTimeModel.ToDate = toDate;
if (editContext != null && dateTimeModel.FromDate != null)
{
FieldIdentifier fromDateField = editContext.Field(nameof(DateTimeModel.FromDate));
editContext.NotifyFieldChanged(fromDateField);
}
}
private void HandleValidSubmit()
{
}
}
Blazor fiddle example
GitHub repository with demo
Solution using IValidatableObject:
To do more complex validation checks, your model can inherit from IValidatableObject interface and implement the Validate method:
public class ExampleModel : IValidatableObject
{
[DataType(DataType.Date)]
public DateTime? FromDate { get; set; }
[DataType(DataType.Date)]
public DateTime? ToDate { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (ToDate < FromDate)
{
yield return new ValidationResult("ToDate must be after FromDate", new[] { nameof(ToDate) });
}
}
}
Blazor fiddle example

Related

How to apply C# Attributes to the type inside of a List for ASP.NET MVC Web Application

I have a model which looks like this:
public class ChartModel
{
public List<int> GoalList { get; set; } = new List<int>();
public List<string?> LabelList { get; set; } = new List<string?>();
[Required]
[StringLength(maximumLength: 75)]
public string? LabelValidation { get; set; }
[DefaultSettingValue("0")]
[Range(1, 100)]
public int GoalValidation { get; set; }
}
I want the attributes above each List to apply to the int and string types, respectively, inside them. Seeing that just doing something like:
[Required]
[StringLength(maximumLength: 75)]
public List<string?> LabelList { get; set; } = new List<string?>();
doesn't work, I made the LabelValidation and GoalValidation variables with the intention of using them in an HTML form where the user inputs values for labels and goals into the list like so:
<input asp-for="LabelList[ind]" class="form-control" placeholder="#ViewBag.Chart.LabelList[ind]" value="#ViewBag.Chart.LabelList[ind]" />
<span asp-validation-for="LabelValidation" class="text-danger"></span>
<input asp-for="GoalList[ind]" class="form-control" placeholder="#ViewBag.Chart.GoalList[ind]" value="#ViewBag.Chart.GoalList[ind]" />
<span asp-validation-for="GoalValidation" class="text-danger"></span>
but that does not seem to work either. How can I apply these attributes to the elements within the lists?
Consider to use custom attributes for this purpose:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Reflection;
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class IntListValidationAttribute : ValidationAttribute
{
public int Minimum { get; private set; }
public int Maximum { get; private set; }
public IntListValidationAttribute(string errorMessage, int minimum, int maximum)
: base(errorMessage)
{
Minimum = minimum;
Maximum = maximum;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var obj = validationContext.ObjectInstance;
if (value is List<int> list)
{
PropertyInfo[] myPropertyInfo = obj.GetType().GetProperties();
for (int i = 0; i < myPropertyInfo.Length; i++)
{
if (myPropertyInfo[i].PropertyType.Equals(typeof(List<int>)))
{
foreach (var item in list)
{
if (item < Minimum || item > Maximum)
{
return new ValidationResult(ErrorMessage);
}
}
return ValidationResult.Success;
}
}
}
return new ValidationResult(ErrorMessage);
}
}
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class StringListValidationAttribute : ValidationAttribute
{
public int MaximumLength { get; private set; }
public StringListValidationAttribute(string errorMessage, int maximumLength)
: base(errorMessage)
{
this.MaximumLength = maximumLength;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var obj = validationContext.ObjectInstance;
if (value is List<string> list)
{
PropertyInfo[] myPropertyInfo = obj.GetType().GetProperties();
for (int i = 0; i < myPropertyInfo.Length; i++)
{
if (myPropertyInfo[i].PropertyType.Equals(typeof(List<string>)))
{
foreach (var item in list)
{
if (item.Length > MaximumLength)
{
return new ValidationResult(ErrorMessage);
}
}
return ValidationResult.Success;
}
}
}
return new ValidationResult(ErrorMessage);
}
}
And then apply the attributes to properties:
public class ChartModel
{
[IntListValidation("Validation error", minimum: 2, maximum: 99)]
public List<int> GoalList { get; set; } = new List<int>();
[StringListValidation("Validation error", maximumLength: 15)]
public List<string> LabelList { get; set; } = new List<string>();
}

Blazor pass ValidationMessage to extended InputText component

I have an ExtendedInputText component which inherits from InputText
#inherits InputText
<div class="flex">
<label class="w-1/2">
#Label
#if(Required){
<span class="text-red-500 ml-1">*</span>
}
</label>
<InputText
class="flex-1 border border-gray-200 bg-white p-2 rounded"
placeholder="#Label"
Value="#Value"
ValueChanged="#ValueChanged"
ValueExpression="#ValueExpression"
Required="#Required"
/>
</div>
#code
{
[Parameter]
public bool Required { get; set; }
[Parameter]
public string Label { get; set; }
}
I intend on using it to replace this
<EditForm Model="Command" OnValidSubmit="OnValidSubmit">
<FluentValidationValidator />
<ValidationSummary />
<div class="">
<label>Title <span class="text-red-500">*</span></label>
<InputText id="Title" #bind-Value="Command.Title" />
<ValidationMessage For="#(() => Command.Title)" />
</div>
<button type="submit" class="p-2 bg-positive-500 text-white rounded">Create</button>
</EditForm>
with this
<EditForm Model="Command" OnValidSubmit="OnValidSubmit">
<FluentValidationValidator />
<ValidationSummary />
<ExtendedInputText Label="Title" Required="true" #bind-Value="Command.Title"/>
<button type="submit" class="p-2 bg-positive-500 text-white rounded">Create</button>
</EditForm>
How would I go about also passing <ValidationMessage For="#(() => Command.Title)" /> to the ExtendedInputText component and rendering it from within?
With the help of Nicola and Shaun, this is the solution that worked for me.
#inherits InputText
<div class="flex">
<label class="w-1/2 text-right font-semibold mr-1 py-2">
#Label
#if (Required)
{
<span class="text-red-500 ml-1">*</span>
}
</label>
<div class="flex-1">
<InputText class="w-full border border-gray-200 bg-white p-2 rounded"
placeholder="#Label"
Value="#Value"
ValueChanged="#ValueChanged"
ValueExpression="#ValueExpression"
Required="#Required"/>
#ValidationFragment
</div>
</div>
#code
{
[Parameter]
public bool Required { get; set; }
[Parameter]
public string Label { get; set; }
private RenderFragment ValidationFragment => (builder) =>
{
var messages = EditContext.GetValidationMessages(FieldIdentifier).ToList();
if(messages is not null && messages.Count > 0)
{
builder.OpenElement(310, "div");
builder.AddAttribute(320, "class", "text-red-500 p-2 w-full");
builder.OpenComponent<ValidationMessage<string>>(330);
builder.AddAttribute(340, "For", ValueExpression);
builder.CloseComponent();
builder.CloseElement();
}
};
}
They key part was the private RenderFragment ValidationFragment which is built programatically to display associated errors stored in the cascading EditContext
The full code for a similar component. Note that the component gets the Validation message, you don't need to pass it.
I think I've included the only dependant class, but I may have missed something.
/// ============================================================
/// Author: Shaun Curtis, Cold Elm Coders
/// License: Use And Donate
/// If you use it, donate something to a charity somewhere
/// ============================================================
using Blazr.SPA.Components;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Rendering;
using System;
using System.Linq;
using System.Linq.Expressions;
#nullable enable
#pragma warning disable CS8622 // Nullability of reference types in type of parameter doesn't match the target delegate (possibly because of nullability attributes).
#pragma warning disable CS8602 // Dereference of a possibly null reference.
namespace Blazr.UIComponents
{
public class FormEditControl<TValue> : ComponentBase
{
[Parameter]
public TValue? Value { get; set; }
[Parameter] public EventCallback<TValue> ValueChanged { get; set; }
[Parameter] public Expression<Func<TValue>>? ValueExpression { get; set; }
[Parameter] public string? Label { get; set; }
[Parameter] public string? HelperText { get; set; }
[Parameter] public string DivCssClass { get; set; } = "mb-2";
[Parameter] public string LabelCssClass { get; set; } = "form-label";
[Parameter] public string ControlCssClass { get; set; } = "form-control";
[Parameter] public Type ControlType { get; set; } = typeof(InputText);
[Parameter] public bool ShowValidation { get; set; }
[Parameter] public bool ShowLabel { get; set; } = true;
[Parameter] public bool IsRequired { get; set; }
[Parameter] public bool IsRow { get; set; }
[CascadingParameter] EditContext CurrentEditContext { get; set; } = default!;
private readonly string formId = Guid.NewGuid().ToString();
private bool IsLabel => this.ShowLabel && (!string.IsNullOrWhiteSpace(this.Label) || !string.IsNullOrWhiteSpace(this.FieldName));
private bool IsValid;
private FieldIdentifier _fieldIdentifier;
private ValidationMessageStore? _messageStore;
private string? DisplayLabel => this.Label ?? this.FieldName;
private string? FieldName
{
get
{
string? fieldName = null;
if (this.ValueExpression != null)
ParseAccessor(this.ValueExpression, out var model, out fieldName);
return fieldName;
}
}
private string MessageCss => CSSBuilder.Class()
.AddClass("invalid-feedback", !this.IsValid)
.AddClass("valid-feedback", this.IsValid)
.Build();
private string ControlCss => CSSBuilder.Class(this.ControlCssClass)
.AddClass("is-valid", this.IsValid)
.AddClass("is-invalid", !this.IsValid)
.Build();
protected override void OnInitialized()
{
if (CurrentEditContext is null)
throw new InvalidOperationException($"No Cascading Edit Context Found!");
if (ValueExpression is null)
throw new InvalidOperationException($"No ValueExpression defined for the Control! Define a Bind-Value.");
if (!ValueChanged.HasDelegate)
throw new InvalidOperationException($"No ValueChanged defined for the Control! Define a Bind-Value.");
CurrentEditContext.OnFieldChanged += FieldChanged;
CurrentEditContext.OnValidationStateChanged += ValidationStateChanged;
_messageStore = new ValidationMessageStore(this.CurrentEditContext);
_fieldIdentifier = FieldIdentifier.Create(ValueExpression);
if (_messageStore is null)
throw new InvalidOperationException($"Cannot set the Validation Message Store!");
var messages = CurrentEditContext.GetValidationMessages(_fieldIdentifier).ToList();
var showHelpText = (messages.Count == 0) && this.IsRequired && this.Value is null;
if (showHelpText && !string.IsNullOrWhiteSpace(this.HelperText))
_messageStore.Add(_fieldIdentifier, this.HelperText);
}
protected void ValidationStateChanged(object sender, ValidationStateChangedEventArgs e)
{
var messages = CurrentEditContext.GetValidationMessages(_fieldIdentifier).ToList();
if (messages != null || messages.Count > 1)
{
_messageStore.Clear();
}
}
protected void FieldChanged(object sender, FieldChangedEventArgs e)
{
if (e.FieldIdentifier.Equals(_fieldIdentifier))
_messageStore.Clear();
}
protected override void OnParametersSet()
{
this.IsValid = true;
{
if (this.IsRequired)
{
this.IsValid = false;
var messages = CurrentEditContext.GetValidationMessages(_fieldIdentifier).ToList();
if (messages is null || messages.Count == 0)
this.IsValid = true;
}
}
}
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
if (IsRow)
builder.AddContent(1, RowFragment);
else
builder.AddContent(2, BaseFragment);
}
private RenderFragment BaseFragment => (builder) =>
{
builder.OpenElement(0, "div");
builder.AddAttribute(10, "class", this.DivCssClass);
builder.AddContent(40, this.LabelFragment);
builder.AddContent(60, this.ControlFragment);
builder.AddContent(70, this.ValidationFragment);
builder.CloseElement();
};
private RenderFragment RowFragment => (builder) =>
{
builder.OpenElement(0, "div");
builder.AddAttribute(10, "class", "row form-group");
builder.OpenElement(20, "div");
builder.AddAttribute(30, "class", "col-12 col-md-3");
builder.AddContent(40, this.LabelFragment);
builder.CloseElement();
builder.OpenElement(40, "div");
builder.AddAttribute(50, "class", "col-12 col-md-9");
builder.AddContent(60, this.ControlFragment);
builder.AddContent(70, this.ValidationFragment);
builder.CloseElement();
builder.CloseElement();
};
private RenderFragment LabelFragment => (builder) =>
{
if (this.IsLabel)
{
builder.OpenElement(110, "label");
builder.AddAttribute(120, "for", this.formId);
builder.AddAttribute(130, "class", this.LabelCssClass);
builder.AddContent(140, this.DisplayLabel);
builder.CloseElement();
}
};
private RenderFragment ControlFragment => (builder) =>
{
builder.OpenComponent(210, this.ControlType);
builder.AddAttribute(220, "class", this.ControlCss);
builder.AddAttribute(230, "Value", this.Value);
builder.AddAttribute(240, "ValueChanged", EventCallback.Factory.Create(this, this.ValueChanged));
builder.AddAttribute(250, "ValueExpression", this.ValueExpression);
builder.CloseComponent();
};
private RenderFragment ValidationFragment => (builder) =>
{
if (this.ShowValidation && !this.IsValid)
{
builder.OpenElement(310, "div");
builder.AddAttribute(320, "class", MessageCss);
builder.OpenComponent<ValidationMessage<TValue>>(330);
builder.AddAttribute(340, "For", this.ValueExpression);
builder.CloseComponent();
builder.CloseElement();
}
else if (!string.IsNullOrWhiteSpace(this.HelperText))
{
builder.OpenElement(350, "div");
builder.AddAttribute(360, "class", MessageCss);
builder.AddContent(370, this.HelperText);
builder.CloseElement();
}
};
// Code lifted from FieldIdentifier.cs
private static void ParseAccessor<T>(Expression<Func<T>> accessor, out object model, out string fieldName)
{
var accessorBody = accessor.Body;
if (accessorBody is UnaryExpression unaryExpression && unaryExpression.NodeType == ExpressionType.Convert && unaryExpression.Type == typeof(object))
accessorBody = unaryExpression.Operand;
if (!(accessorBody is MemberExpression memberExpression))
throw new ArgumentException($"The provided expression contains a {accessorBody.GetType().Name} which is not supported. {nameof(FieldIdentifier)} only supports simple member accessors (fields, properties) of an object.");
fieldName = memberExpression.Member.Name;
if (memberExpression.Expression is ConstantExpression constantExpression)
{
if (constantExpression.Value is null)
throw new ArgumentException("The provided expression must evaluate to a non-null value.");
model = constantExpression.Value;
}
else if (memberExpression.Expression != null)
{
var modelLambda = Expression.Lambda(memberExpression.Expression);
var modelLambdaCompiled = (Func<object?>)modelLambda.Compile();
var result = modelLambdaCompiled();
if (result is null)
throw new ArgumentException("The provided expression must evaluate to a non-null value.");
model = result;
}
else
throw new ArgumentException($"The provided expression contains a {accessorBody.GetType().Name} which is not supported. {nameof(FieldIdentifier)} only supports simple member accessors (fields, properties) of an object.");
}
}
}
#pragma warning restore CS8622
#pragma warning restore CS8602
#nullable disable
and CSSBuilder
/// ============================================================
/// Author: Shaun Curtis, Cold Elm Coders
/// License: Use And Donate
/// If you use it, donate something to a charity somewhere
/// ============================================================
using System.Collections.Generic;
using System.Text;
using System.Linq;
namespace Blazr.SPA.Components
{
public class CSSBuilder
{
private Queue<string> _cssQueue = new Queue<string>();
public static CSSBuilder Class(string cssFragment = null)
{
var builder = new CSSBuilder(cssFragment);
return builder.AddClass(cssFragment);
}
public CSSBuilder()
{
}
public CSSBuilder (string cssFragment)
{
AddClass(cssFragment);
}
public CSSBuilder AddClass(string cssFragment)
{
if (!string.IsNullOrWhiteSpace(cssFragment)) _cssQueue.Enqueue(cssFragment);
return this;
}
public CSSBuilder AddClass(IEnumerable<string> cssFragments)
{
if (cssFragments != null)
cssFragments.ToList().ForEach(item => _cssQueue.Enqueue(item));
return this;
}
public CSSBuilder AddClass(string cssFragment, bool WhenTrue)
{
if (WhenTrue) return this.AddClass(cssFragment);
return this;
}
public CSSBuilder AddClassFromAttributes(IReadOnlyDictionary<string, object> additionalAttributes)
{
if (additionalAttributes != null && additionalAttributes.TryGetValue("class", out var val))
_cssQueue.Enqueue(val.ToString());
return this;
}
public CSSBuilder AddClassFromAttributes(IDictionary<string, object> additionalAttributes)
{
if (additionalAttributes != null && additionalAttributes.TryGetValue("class", out var val))
_cssQueue.Enqueue(val.ToString());
return this;
}
public string Build(string CssFragment = null)
{
if (!string.IsNullOrWhiteSpace(CssFragment)) _cssQueue.Enqueue(CssFragment);
if (_cssQueue.Count == 0)
return string.Empty;
var sb = new StringBuilder();
foreach(var str in _cssQueue)
{
if (!string.IsNullOrWhiteSpace(str)) sb.Append($" {str}");
}
return sb.ToString().Trim();
}
}
}
It looks like this in action:
I use the following code for a component I've created LabelText but should be used for your case:
public partial class LabelText<T>: ComponentBase
{
[Parameter] public Expression<Func<T>> For { get; set; }
[Parameter] public RenderFragment ChildContent { get; set; }
private FieldIdentifier _fieldIdentifier;
...
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "label");
builder.AddMultipleAttributes(1, AdditionalAttributes);
builder.AddAttribute(2, "for", _fieldIdentifier.FieldName);
builder.AddContent(3, label + GetRequired());
builder.CloseElement();
}
protected override void OnParametersSet()
{
if (CurrentEditContext == null)
{
throw new InvalidOperationException($"{GetType()} requires a cascading parameter " +
$"of type {nameof(EditContext)}. For example, you can use {GetType()} inside " +
$"an {nameof(EditForm)}.");
}
if (For == null) // Not possible except if you manually specify T
{
throw new InvalidOperationException($"{GetType()} requires a value for the " +
$"{nameof(For)} parameter.");
}
_fieldIdentifier = FieldIdentifier.Create(For);
}
UPDATE
I can't explain better then the excellent piece of code from #MrC
private RenderFragment ValidationFragment => (builder) =>
{
if (this.ShowValidation && !this.IsValid)
{
builder.OpenElement(310, "div");
builder.AddAttribute(320, "class", MessageCss);
builder.OpenComponent<ValidationMessage<TValue>>(330);
builder.AddAttribute(340, "For", this.ValueExpression);
builder.CloseComponent();
builder.CloseElement();
}
else if (!string.IsNullOrWhiteSpace(this.HelperText))
{
builder.OpenElement(350, "div");
builder.AddAttribute(360, "class", MessageCss);
builder.AddContent(370, this.HelperText);
builder.CloseElement();
}
};
you only need to add a paremeter like ValidationMessage="(() => Command.Title)" and this RenderFragment does the job for you.

Custom Blazor DateTime using inherits InputBase<DateTime> with ValidationMessage

I have the following custom date input:
CustomDateInput.razor
<input type="date"
class="#CssClass"
#bind=CurrentValue
#attributes=AdditionalAttributes />
#if (ValidationFor is not null)
{
<ValidationMessage For="#ValidationFor" />
}
#using System.Linq.Expressions
#inherits InputBase<DateTime?>
#code {
[Parameter] public Expression<Func<DateTime>>? ValidationFor { get; set; }
protected override bool TryParseValueFromString(string? value, out DateTime? result, out string validationErrorMessage)
{
result = CurrentValue;
validationErrorMessage = "";
return true;
}
}
FooPage.razor
<CustomInputDate #bind-Value="FooDate" ValidationFor="() => FooDate"></CustomInputDate>
FooPage.razor.cs
public DateTime? FooDate { get; set; }
However, I get errors:
How can I modify my CustomDateInput to allow for a Validation parameter to show a ValidationMessage?
The component. I've lifted the BuildRenderTree code from InputDate and added in the ValidationMessage component. You need to do it this way as I don't know a way to do the For binding in Razor. I've tied the For directly into the ValueExpression property of InputBase. You'll probably need to add a bit of formatting/css to prettify it.
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Rendering;
using System;
namespace Blazor.Starter.Components.TestComponents
{
public class CustomDate : InputDate<DateTime?>
{
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "input");
builder.AddMultipleAttributes(1, AdditionalAttributes);
builder.AddAttribute(2, "type", "date");
builder.AddAttribute(3, "class", CssClass);
builder.AddAttribute(4, "value", BindConverter.FormatValue(CurrentValueAsString));
builder.AddAttribute(5, "onchange", EventCallback.Factory.CreateBinder<string?>(this, __value => CurrentValueAsString = __value, CurrentValueAsString));
builder.CloseElement();
builder.OpenComponent<ValidationMessage<DateTime?>>(6);
builder.AddAttribute(7, "For", this.ValueExpression);
builder.CloseComponent();
}
}
}
And a demo page. There's a button to manually set an error message in the validationMessageStore.
#page "/editortest"
<h3>EditorTest</h3>
<EditForm EditContext="editContext">
<div>
<CustomDate #bind-Value="model.Date"></CustomDate>
</div>
</EditForm>
<div class="m-3 p-3"><input #bind-value="_errormessage"><button class="btn btn-dark ms-2" #onclick="SetError">Set Error</button></div>
#code {
private dataModel model { get; set; } = new dataModel();
private EditContext editContext;
private string _errormessage { get; set; } = "Error in date";
protected override Task OnInitializedAsync()
{
this.editContext = new EditContext(model);
return base.OnInitializedAsync();
}
private void SetError( MouseEventArgs e)
{
var validationMessageStore = new ValidationMessageStore(this.editContext);
validationMessageStore.Clear();
var fi = new FieldIdentifier(this.model, "Date");
validationMessageStore.Add(fi, _errormessage);
}
public class dataModel
{
public string Email { get; set; }
public DateTime? Date { get; set; }
}
}
I have different way to reach the same result, you can try it!:
[Parameter] public bool ShowError { get; set; } = false;
<input type="date"
class="#CssClass"
#bind=CurrentValue
#attributes=AdditionalAttributes #onfocus="#(() => ShowError =true)"/>
#if(ShoError)
{
foreach(var msg in EditCotext.GetValidationMessages(FieldIdententifier))
{
<div class="alidation-message">#msg</div>
}
}
you need aslo to keep you code TryParseValueFromString

client side validation on custom type property using custom validator

I have a tricky requirement where i have to compare two model properties which are of custom class types. Server side validation is straight forward but achieving client side validation using jquery unobtrusive is not clear . When the mvc renders the html i am not seeing data-val attributes appended
public class CommunicationViewModel
{
public PhoneViewModel MobilePhone { get; set; }
[ComparePhoneLocalized("CommunicationView.MobilePhone")]
public PhoneViewModel ConfirmMobilePhoneNumber { get; set; }
........
........
}
public class PhoneViewModel
{
public string Area { get; set; }
public string Number { get; set; }
public string Extension { get; set; }
public string CountryCode { get; set; }
}
Custom validator
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public class CompareConditionalLocalizedAttribute : CompareAttribute, IClientValidatable
{
private readonly object _typeId = new object();
private const string ErrorMsg = "Rentered mobile number does not match the original";
public string FieldName { get; set; }
//public MessageManagement.MessageContext MessageContext { get; set; }
//public MessageCode MessageCode { get; set; }
public new string OtherProperty { get; set; }
public CompareConditionalLocalizedAttribute(string otherProperty)
: base(otherProperty)
{
OtherProperty = otherProperty;
}
public override string FormatErrorMessage(string name)
{
//** This error message will eventually be driven by resource provider factory **//
return ErrorMsg;
//var msg = Message.Create(MessageCode, MessageStatusType.Error, FieldName);
//var messageRepository = ServeContext.Current.ResolveInstance<IMessageRepository>();
//msg = messageRepository.MapMessage(msg, MessageContext);
//return msg.MessageText;
}
protected override ValidationResult IsValid(object value, ValidationContext
validationContext)
{
if (String.IsNullOrEmpty(OtherProperty)) return null;
var otherProperty = validationContext.ObjectType.GetProperty(OtherProperty);
var compareTo = (string)otherProperty.GetValue(validationContext.ObjectInstance, null);
if (value == null) return null;
if (compareTo.Equals(value.ToString(), StringComparison.Ordinal))
{
return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
}
return ValidationResult.Success;
}
public new IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata
metadata, ControllerContext context)
{
var rule = new ModelClientValidationRule
{
ErrorMessage = FormatErrorMessage(FieldName),
ValidationType = "compareemail"
};
rule.ValidationParameters.Add("otherproperty", OtherProperty);
yield return rule;
}
}
JQuery Wire up
jQuery.validator.unobtrusive.adapters.add("comparephone", "[otherproperty]", function
(options) {
options.rules["comparephone"] = options.params.otherproperty;
options.messages["comparephone"] = options.message;
});
jQuery.validator.addMethod("comparephone", function (value, element, params) {
var compareTo = $('[name="' + params + '"]').val();
if (value == compareTo) {
return true;
}
return false;
});

How to create regularExpressionAttribute with dynamic pattern come from model property

public class City
{
[DynamicReqularExpressionAttribute(PatternProperty = "RegEx")]
public string Zip {get; set;}
public string RegEx { get; set;}
}
I woud like to create this attribute where the pattern come from an other property and not declare static like in the original RegularExpressionAttribute.
Any ideas would be appreciated - Thanks
Something among the lines should fit the bill:
public class DynamicRegularExpressionAttribute : ValidationAttribute
{
public string PatternProperty { get; set; }
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
PropertyInfo property = validationContext.ObjectType.GetProperty(PatternProperty);
if (property == null)
{
return new ValidationResult(string.Format("{0} is unknown property", PatternProperty));
}
var pattern = property.GetValue(validationContext.ObjectInstance, null) as string;
if (string.IsNullOrEmpty(pattern))
{
return new ValidationResult(string.Format("{0} must be a valid string regex", PatternProperty));
}
var str = value as string;
if (string.IsNullOrEmpty(str))
{
// We consider that an empty string is valid for this property
// Decorate with [Required] if this is not the case
return null;
}
var match = Regex.Match(str, pattern);
if (!match.Success)
{
return new ValidationResult(this.FormatErrorMessage(validationContext.DisplayName));
}
return null;
}
}
and then:
Model:
public class City
{
[DynamicRegularExpression(PatternProperty = "RegEx")]
public string Zip { get; set; }
public string RegEx { get; set; }
}
Controller:
public class HomeController : Controller
{
public ActionResult Index()
{
var city = new City
{
RegEx = "[0-9]{5}"
};
return View(city);
}
[HttpPost]
public ActionResult Index(City city)
{
return View(city);
}
}
View:
#model City
#using (Html.BeginForm())
{
#Html.HiddenFor(x => x.RegEx)
#Html.LabelFor(x => x.Zip)
#Html.EditorFor(x => x.Zip)
#Html.ValidationMessageFor(x => x.Zip)
<input type="submit" value="OK" />
}
override the Validate method that takes the ValidationContext as a parameter, use the ValidationContext to get the regex string from the related property and apply the regex, returning the matched value.

Categories

Resources