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.
Related
I'm trying to work out how to validate a deeply nested form structure (sections/panels/fields) where the fields themselves are stored as a dictionary. I have managed to fix up the paths and such so that the standard ValidationMessage componet displays a reasonable human readable error, and all of my validations do appear to function, unfortunately the Css Class setting has ceased to function.
When using a simple form layer (unnested Poco) the classes are applied automatically - my SyncFusion controls receive their 'e-error' or 'e-success' classes and change colour accordingly - when using a home made Validator, the colours don't function any more. I also tried setting my own CssClassProvider with EditContext.SetFieldCssClassProvider, and a breakpoint on the GetFieldCssClass function is never hit.
Effectively whilst my calculated FieldIdentifiers work correctly with ValidationMessage, that doesn't lead to any kind of Css Update.
Is there some kind of trigger that needs to be called from my FluentValidation Validator to kick off the CssClass mechanism?
Here's some code - note that the EditForm was in the parent page of PanelLayout and it's Model is of type PanelLayoutData - I didn't want to have to paste all of that.
PanelLayout.razor
<article class="st-vscroll bg-body pb-5 st-theme-#Theme">
<PanelLayoutValidator />
<ValidationSummary />
#{
int sectIndex = 0;
foreach(var panel in Data.panelData) {
int panIndex = 0;
while (panIndex < panel.Panels.Count)
{
var pan1 = panel.Panels[panIndex++];
<FieldListPanel Title=#pan1.Title DataDictionary=#(pan1.DisplayDictionary) LabelAbove=#true />
}
}
}
</article>
#code {
[CascadingParameter(Name = "Theme")] public string Theme { get; set; }
[CascadingParameter(Name = "EditMode")] public bool EditMode { get; set; }
[CascadingParameter] public EditContext EditContext { get; set; }
[Parameter] public clientmodels.PanelLayoutData Data { get; set; } = null;
protected override void OnParametersSet()
{
sectionRefs = new ElementReference[Data.panelData.Count];
if (EditContext != null) {
EditContext.SetFieldCssClassProvider(new _PanelLayoutFieldCssClassProvider());
}
}
}
FieldListPanel.razor
<div class="card rounded-0 border-0">
#if (!string.IsNullOrEmpty(Title))
{
<div class="card-header border-0 mt-3">
<h3 class="display-6">#Title</h3>
</div>
}
<div class="card-body">
#if (DataDictionary?.Any() ?? false)
{
#foreach (var kv in DataDictionary) {
<div class="row mb-3">
#if (!(LabelAbove && kv.Value?.DisplayName == ""))
{
<div class=#((LabelAbove ? "col-12" : "col-4"))>
#(kv.Value?.DisplayName ?? kv.Key)#if (kv.Value.IsRequired) { <span style="required-value">*</span> }
</div>
}
<div class=#((LabelAbove ? "col-12" : "col-8"))>
#if (kv.Value?.Template != null)
{
#kv.Value?.Template
} else
{
//When there is no template then it's just text - rendering in edit mode will require a text box
if (Editable && EditMode && kv.Value.IsEditable)
{
<SfTextBox Value=#kv.Value.Value ValueChange="#((__v) => updateDictValue(__v, kv.Key))" /><br />
<ValidationMessage For="() => kv.Value.Value" />
} else
{
#kv.Value?.Value
}
}
</div>
</div>
}
}
</div>
</div>
#code {
[Parameter]
public string Title { get; set; } = "";
[Parameter]
public IDictionary<string, clientmodels.FieldDisplayData> DataDictionary { get; set; } = null;
[Parameter]
public bool LabelAbove { get; set; } = false;
[Parameter] public bool Editable { get; set; } = true;
[CascadingParameter(Name = "EditMode")] public bool EditMode { get; set; }
[Parameter]
public EventCallback OnChanged { get; set; }
private async Task updateDictValue(ChangedEventArgs e, string key)
{
if (DataDictionary.ContainsKey(key))
{
DataDictionary[key].Value = e.Value;
await OnChanged.InvokeAsync();
}
}
}
PanelLayoutValidator.cs
namespace CustomStyle.Client.Code
{
public class _PanelLayoutValidationState
{
public string FullPath { get; set; }
}
public class _PanelLayoutFieldCssClassProvider : FieldCssClassProvider
{
public override string GetFieldCssClass(EditContext editContext, in FieldIdentifier fieldIdentifier)
{
var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();
return isValid ? "e-success" : "e-error";
}
}
public class _PanelLayoutValidator : AbstractValidator<PanelLayoutData>
{
public _PanelLayoutValidator()
{
RuleForEach(l => l.panelData)
.ChildRules(l => {
l.RuleForEach(s => s.Panels)
.ChildRules(s => {
//We apply rules for [Required] [MinLength] [MaxLength] [CreditCard] [EmailAddress] [Range] [RegularExpression]
//Configure property names
var ddeach = s.RuleForEach(p => p.DisplayDictionary);
ddeach.ChildRules(kvconfig =>
{
kvconfig.RuleFor(kv => kv.Value.Value).Configure(cfg =>
{
cfg.MessageBuilder = context =>
{
context.MessageFormatter.AppendPropertyName(context.PropertyName);
return context.GetDefaultMessage();
};
});
});
//Non parametric validations
ddeach.ChildRules(kvconfig =>
{
//IsRequired
kvconfig.RuleFor(kv => kv.Value.Value)
.NotEmpty().When(x => x.Value != null && x.Value.IsRequired).WithMessage("{ParsedPropertyName} cannot be empty");
//CreditCard
kvconfig.RuleFor(kv => kv.Value.Value)
.CreditCard().When(x => x.Value != null && x.Value.IsCreditCard).WithMessage("{ParsedPropertyName} should be a Credit or Debit card number");
//EmailAddress
kvconfig.RuleFor(kv => kv.Value.Value)
.EmailAddress().When(x => x.Value != null && x.Value.IsCreditCard).WithMessage("{ParsedPropertyName} should be an Email Address");
});
//Parametric validations
ddeach.ChildRules(kvconfig =>
{
//MinLength
kvconfig.RuleFor(kv => new { Value = kv.Value.Value, Config = kv.Value })
.Must(vl => vl.Value.Length >= vl.Config.MinLength)
.When(x => x.Value != null && x.Value.MinLength != null)
.WithMessage(x => $"{{ParsedPropertyNameVV}} must have at least {x.Value.MinLength} characters");
//MaxLength
kvconfig.RuleFor(kv => new { Value = kv.Value.Value, Config = kv.Value })
.Must(vl => vl.Value.Length <= vl.Config.MaxLength)
.When(x => x.Value != null && x.Value.MaxLength != null)
.WithMessage(x => $"{{ParsedPropertyNameVV}} must have at most {x.Value.MaxLength} characters");
//Range
kvconfig.RuleFor(kv => new { Value = int.Parse(kv.Value.Value), Config = kv.Value })
.Must(vl => vl.Value >= vl.Config.Range[0] && vl.Value <= vl.Config.Range[1])
.When(x => x.Value != null && x.Value.Range != null)
.WithMessage(x => $"{{ParsedPropertyNameVV}} must be between {x.Value.Range[0]} and {x.Value.Range[1]}");
//Regex
kvconfig.RuleFor(kv => new { Value = kv.Value.Value, Config = kv.Value })
.Must(vl => System.Text.RegularExpressions.Regex.IsMatch(vl.Value, vl.Config.RegularExpression))
.When(x => x.Value != null && x.Value.RegularExpression != null)
.WithMessage(x => $"{{ParsedPropertyNameVV}} does not match the expected pattern");
});
//ToDo: Add rules for RefPoco routes based on object data annotation attributes
});
});
}
}
public class PanelLayoutValidator : ComponentBase
{
private readonly static char[] separators = new[] { '.', '[' };
private _PanelLayoutValidator validator;
[CascadingParameter] private EditContext EditContext { get; set; }
protected override void OnInitialized()
{
validator = new _PanelLayoutValidator();
var messages = new ValidationMessageStore(EditContext);
// Revalidate when any field changes, or if the entire form requests validation
// (e.g., on submit)
EditContext.OnFieldChanged += (sender, eventArgs)
=> ValidateModel((EditContext)sender, messages);
EditContext.OnValidationRequested += (sender, eventArgs)
=> ValidateModel((EditContext)sender, messages);
}
private string GetParsedPropertyName(EditContext context, FieldIdentifier id, string PropertyName)
{
//process the property path to calculate the property description
//If we're using the expected format for a dictionary field, we can read the display name and the key
var model = context.Model as PanelLayoutData;
var match = System.Text.RegularExpressions.Regex.Match(PropertyName, #"^panelData\[(?<section_index>[^\]]+)\].Panels\[(?<panel_index>[^\]]+)\].DisplayDictionary\[(?<field_key>[^\]]+)\].(?<target>Value|RefPoco)");
if (match.Success)
{
var section_index = int.Parse(match.Groups["section_index"].Value);
var section_name = model.panelData[section_index].Title;
var panel_index = int.Parse(match.Groups["panel_index"].Value);
var panel_name = model.panelData[section_index].Panels[panel_index].Title;
var property_name = "";
if (match.Groups["target"].Value == "Value")
{
var field_key_index = int.Parse(match.Groups["field_key"].Value);
var dict = model.panelData[section_index].Panels[panel_index].DisplayDictionary;
var field_key = dict.Keys.Skip(field_key_index).First();
property_name = dict[field_key].DisplayName ?? field_key;
} else
{
//TODO: Expand this to grab the property and look for DisplayName attributes
property_name = id.FieldName;
}
if (!string.IsNullOrEmpty(section_name)) {
section_name += "/";
}
if (!string.IsNullOrEmpty(panel_name))
{
panel_name += "/";
}
return $"{section_name}{panel_name}{property_name}";
} else {
//we have no section info so just pick up the property name
//TODO: Expand this to grab the property and look for DisplayName attributes
return id.FieldName;
}
}
private void ValidateModel(EditContext editContext, ValidationMessageStore messages)
{
var validationResult = validator.Validate((PanelLayoutData)editContext.Model);
messages.Clear();
foreach (var error in validationResult.Errors)
{
FieldIdentifier fieldIdentifier = default(FieldIdentifier);
var msg = error.ErrorMessage;
if (msg.Contains("{ParsedPropertyName}")) {
fieldIdentifier = ToFieldIdentifier(editContext, error.PropertyName);
msg = msg.Replace("{ParsedPropertyName}", GetParsedPropertyName(editContext, fieldIdentifier, error.PropertyName));
}
if (msg.Contains("{ParsedPropertyNameVV}"))
{
fieldIdentifier = ToFieldIdentifier(editContext, $"{error.PropertyName}.Value");
msg = msg.Replace("{ParsedPropertyNameVV}", GetParsedPropertyName(editContext, fieldIdentifier, $"{error.PropertyName}.Value"));
}
messages.Add(fieldIdentifier, msg);
}
editContext.NotifyValidationStateChanged();
}
private static FieldIdentifier ToFieldIdentifier(EditContext editContext, string propertyPath)
{
// This method parses property paths like 'SomeProp.MyCollection[123].ChildProp'
// and returns a FieldIdentifier which is an (instance, propName) pair. For example,
// it would return the pair (SomeProp.MyCollection[123], "ChildProp"). It traverses
// as far into the propertyPath as it can go until it finds any null instance.
var obj = editContext.Model;
while (true)
{
var nextTokenEnd = propertyPath.IndexOfAny(separators);
if (nextTokenEnd < 0)
{
return new FieldIdentifier(obj, propertyPath);
}
var nextToken = propertyPath.Substring(0, nextTokenEnd);
propertyPath = propertyPath.Substring(nextTokenEnd + 1);
object newObj;
if (nextToken.EndsWith("]"))
{
nextToken = nextToken.Substring(0, nextToken.Length - 1);
var tobj = obj.GetType();
if (obj is IDictionary)
{
//fluent validation indicates index in dictionary as an integer - dictionaries don't index like that
//grab the key at the given index
var kprop = tobj.GetProperty("Keys");
var keys = (ICollection)kprop.GetValue(obj);
object key = keys.Cast<object>().Skip(int.Parse(nextToken)).First();
var prop = tobj.GetProperty("Item");
newObj = prop.GetValue(obj, new object[] { key });
}
else
{
// It's an indexer
// This code assumes C# conventions (one indexer named Item with one param)
var prop = tobj.GetProperty("Item");
var indexerType = prop.GetIndexParameters()[0].ParameterType;
var indexerValue = Convert.ChangeType(nextToken, indexerType);
newObj = prop.GetValue(obj, new object[] { indexerValue });
}
}
else
{
// It's a regular property
var prop = obj.GetType().GetProperty(nextToken);
if (prop == null)
{
throw new InvalidOperationException($"Could not find property named {nextToken} on object of type {obj.GetType().FullName}.");
}
newObj = prop.GetValue(obj);
}
if (newObj == null)
{
// This is as far as we can go
return new FieldIdentifier(obj, nextToken);
}
obj = newObj;
}
}
}
}
PanelModels.cs
namespace CustomStyle.Client.Models
{
public class PanelData
{
public string Title { get; set; }
public Dictionary<string, FieldDisplayData> DisplayDictionary { get; set; } = new();
public bool IsFullWidth { get; set; } = false;
public bool IsSpacer { get; set; } = false;
public bool LabelAbove { get; set; } = false;
}
public class PanelSectionData
{
[Required]
public string Title { get; set; }
public List<PanelData> Panels { get; set; } = new();
}
public class PanelLayoutData : PageLayoutData
{
public string idPrefix { get; set; }
public List<PanelSectionData> panelData { get; set; } = new();
}
public class FieldDisplayData
{
public FieldDisplayData()
{
}
public FieldDisplayData(string value, bool isRequired = false)
{
DisplayName = null;
Value = value;
Template = null;
IsRequired = isRequired;
}
public FieldDisplayData(RenderFragment template, bool isRequired = false)
{
DisplayName = null;
Value = null;
Template = template;
IsRequired = isRequired;
}
public FieldDisplayData(string displayName, string value, bool isRequired = false)
{
DisplayName = displayName;
Value = value;
Template = null;
IsRequired = isRequired;
}
public FieldDisplayData(string displayName, RenderFragment template, bool isRequired = false)
{
DisplayName = displayName;
Value = null;
Template = template;
IsRequired = isRequired;
}
public string DisplayName { get; set; } = null;
public string Value { get; set; } = null;
public RenderFragment Template { get; set; } = null;
//If the Template references a child of the overall panel data tree,
//add a reference here to allow the validator to see it
public object RefPoco { get; set; } = null;
public bool IsEditable { get; set; } = true;
public bool IsRequired { get; set; } = false;
public int? MinLength { get; set; } = null;
public int? MaxLength { get; set; } = null;
}
}
Hopefully somebody might just know what I messed up.
Thanks.
This is how things work in the standard Blazor Input Controls, which is probably similar to the SynFusion controls (but as they are proprietary the jury is out).
The input control builds a FieldIdentifier from the ValueExpression that either you provide manually or the Razor compiler builds for you from a #bind-Value definition. When a component renders, it uses this FieldIdentifier to check for validation messages in the Validation Store and then applies the neccessary Css settings through a FieldCssClassProvider.
So either:
Your input components aren't re-rendering when the validation state changes in the form, or
The FieldIdentifier constructed by the input control doesn't match the one used to identify the validation message in the Validation Store, or
The Syncfusion controls operate to a different set of rules.
As I don't use Syncfusion and you're question is a bit of a wall of code without context I can only offer pointers as to where the problem might be. Hopefully someone with Syncfusion knowledge will provide a more solid answer. Good luck.
I'm tired to search on web a solution for this. Basicly i am using InputBase to extend the normal inputbox to a custom component. For single selection in its ok, but turns complicate when i have mutiple selection "select multiple="multiple""
So here is the code:
File: XDropDownMultiSelect.razor
#using System.Linq.Expressions
#typeparam T
#inherits InputBase<T>
#if (!string.IsNullOrEmpty(Label))
{
<label class="form-label">#Label</label>
}
<select #bind="CurrentValue" class="form-control select2 #CssClass" id="#Id" #attributes="AdditionalAttributes" multiple>
#if (DropdownValues != null)
{
foreach (var cursor in DropdownValues)
{
<option value="#cursor.Key">#cursor.Value</option>
}
}
</select>
#code {
[Inject] public IJSRuntime _js { get; set; }
[Parameter, EditorRequired] public string Id { get; set; }
[Parameter] public string Label { get; set; }
[Parameter] public Expression<Func<T>> ValidationFor { get; set; }
[Parameter] public bool ShowDefaultOption { get; set; } = true;
[Parameter] public Dictionary<string, string> DropdownValues { get; set; }
[Parameter] public string Selected { get; set; }
protected override bool TryParseValueFromString(string value, out T result, out string validationErrorMessage)
{
if (typeof(T) == typeof(string))
{
result = (T)(object)value;
validationErrorMessage = null;
return true;
}
else if (typeof(T) == typeof(string[]))
{
result = (T)(object)(new string[] { value });
validationErrorMessage = null;
return true;
}
else if (typeof(T) == typeof(Guid))
{
Guid.TryParse(value, out var parsedValue);
result = (T)(object)parsedValue;
validationErrorMessage = null;
return true;
}
else if (typeof(T).IsEnum)
{
try
{
result = (T)Enum.Parse(typeof(T), value);
validationErrorMessage = null;
return true;
}
catch (ArgumentException)
{
result = default;
validationErrorMessage = $"The {FieldIdentifier.FieldName} field is not valid.";
return false;
}
}
throw new InvalidOperationException($"{GetType()} does not support the type '{typeof(T)}'.");
}
}
I use "CurrentValue" instead of "CurrentValueAsString" because is an array no string, and if i set CurrentValueAsString will have a render json problem...
Now i call by simple:
<XDropDownMultiSelect #bind-Value="usersSelected" Id="test" DropdownValues="usersAll" />
#code{
public string[] usersSelected { get; set; } = new [] { "user1" };
public string[] usersAll{ get; set; } = new [] { "user1", "user2", "user3" };
Its working, but dosen´t bind the new selection values to my selectValues object.
i found how to use CurrentValueAsString
protected override string FormatValueAsString(T? value)
{
if( value != null)
{
return value.ToJson(); //this is extension my to convert any object to json format.
}
return base.FormatValueAsString(value);
}
But this not update the source model with new selections.
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
I'm trying to come up with a Blazor implementation of using an array type model with multiple checkboxes.
Vue component:
<template>
<div>
<b-form-group label="Using sub-components:">
<b-form-checkbox-group id="checkbox-group-2" v-model="selected" name="flavour-2">
<b-form-checkbox value="orange">Orange</b-form-checkbox>
<b-form-checkbox value="apple">Apple</b-form-checkbox>
<b-form-checkbox value="pineapple">Pineapple</b-form-checkbox>
<b-form-checkbox value="grape">Grape</b-form-checkbox>
</b-form-checkbox-group>
</b-form-group>
<div>Selected: <strong>{{ selected }}</strong></div>
</div>
</template>
<script>
export default {
data() {
return {
selected: [], // Must be an array reference!
}
}
}
</script>
Blazor component:
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="#id" name="#name" #onchange="#((ChangeEventArgs) => CheckedChanged(ChangeEventArgs, value))">
<label class="custom-control-label" for="#id">#label</label>
</div>
#code {
[Parameter]
public string id { get; set; }
[Parameter]
public string name { get; set; }
[Parameter]
public object value { get; set; }
[Parameter]
public List<object> model { get; set; }
[Parameter]
public EventCallback<List<object>> modelChanged { get; set; }
[Parameter]
public string label { get; set; }
protected override void OnInitialized()
{
base.OnInitialized();
}
protected void CheckedChanged(ChangeEventArgs args, object value)
{
if(!model.Any(i => i == value))
{
model.Add(value);
}
else
{
model.Remove(value);
}
}
}
Usage:
#foreach (string timezone in DistinctTimezones)
{
<BCheckbox #bind-model="#FilterTimezones" value="#timezone" label="#timezone" id="#(string.Format("timezone_{0}", timezone))" name="#(string.Format("timezone_{0}", timezone))" />
}
<p>Selected:</p>
#foreach(var timezone in FilterTimezones)
{
#timezone
}
#code {
protected List<string> DistinctTimezones { get; set; } = new List<string>{"Central", "Eastern"};
protected List<object> FilterTimezones { get; set; } = new List<object>();
}
When I check the checkboxes, the FilterTimezone object doesn't get updated with the values from checked checkboxes. Is this something that is already possible and I am overcomplicating it? I'm only aware of binding values to a non-collection type.
I'd do it like that (string is here just to simplify to get the idea)
CollectionCheckBox.razor
<input type="checkbox" #bind=isChecked />
#code
{
[Parameter]
public string Value { get; set; }
[Parameter]
public List<string> Model {get; set;}
private bool isChecked
{
get => Model.Any(el => el == Value);
set
{
if(value)
{
if(!Model.Any(el => el == Value) Model.Add(Value);
}
else
Model.Remove(Value);
}
}
}
Then in parent component you just do
<CollectionCheckBox Model="#Model", Value="Orange" />
<CollectionCheckBox Model="#Model", Value="Apple" />
<CollectionCheckBox Model="#Model", Value="Pineapple" />
<CollectionCheckBox Model="#Model", Value="Grape" />
#code
{
private List<string> Model = new List<string();
}
I used jquery select2 in blazor server side how i can bind selected value
<InputSelect class="form-control select2" #bind-Value="#purchaseSearch.PriorityId" id="search-priorityId">
<option value="">All</option>
#foreach (var priority in priorities)
{
<option value="#priority.Id">#priority.Name</option>
}
</InputSelect>
I has created a custom component select with select2 library for blazor.
I hope this is example for you.
- Select2.razor:
#typeparam TValue
#inherits InputBase<TValue>
#if (!string.IsNullOrWhiteSpace(Label))
{
<label class="form-control-label" for="#Id">
#Label
#if (Required)
{
<font color="red">(*)</font>
}
</label>
}
else
{
<LabelFor FieldIdentifier="FieldIdentifier"></LabelFor>
}
<select id="#Id" class="form-control select2" style="width: 100%;" >
<option #key="null" value="null">--- Chọn ---</option>
#if (Datasource != null)
#foreach (var item in Datasource)
{
if (item.Key == Value?.ToString())
{
<option #key="#item.Key" value="#item.Key" selected="selected">
#((MarkupString)#item.Value)
</option>
}
else
{
<option #key="#item.Key" value="#item.Key">
#((MarkupString)#item.Value)
</option>
}
}
</select>
<div class="form-control-validation">
<CustomValidationMessage Field="FieldIdentifier" TValue="string" />
</div>
Select2.razor.cs
public partial class SelectWithFilter<TValue> : InputBase<TValue>
{
[Parameter] public string Id { get; set; }
[Parameter] public string Label { get; set; }
[Parameter] public bool Required { get; set; }
//[Parameter] public Expression<Func<string>> ValidationFor { get; set; }
[Parameter] public ICollection<KeyValuePair<string, string>> Datasource { get; set; }
[Inject] IJSRuntime JSRuntime { get; set; }
public DotNetObjectReference<SelectWithFilter<TValue>> DotNetRef;
protected override bool TryParseValueFromString(string value, out TValue result, out string validationErrorMessage)
{
if (value == "null")
{
value = null;
}
if (typeof(TValue) == typeof(string))
{
result = (TValue)(object)value;
validationErrorMessage = null;
return true;
}
else if (typeof(TValue) == typeof(int) || typeof(TValue) == typeof(int?))
{
int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedValue);
result = (TValue)(object)parsedValue;
validationErrorMessage = null;
return true;
}
throw new InvalidOperationException($"{GetType()} does not support the type '{typeof(TValue)}'.");
}
protected override void OnInitialized()
{
base.OnInitialized();
DotNetRef = DotNetObjectReference.Create(this);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
await JSRuntime.InvokeVoidAsync("select2Component.init", Id);
await JSRuntime.InvokeVoidAsync("select2Component.onChange", Id, DotNetRef, "Change_SelectWithFilterBase");
}
}
[JSInvokable("Change_SelectWithFilterBase")]
public void Change(string value)
{
if (value == "null")
{
value = null;
}
if (typeof(TValue) == typeof(string))
{
CurrentValue = (TValue)(object)value;
}
else if (typeof(TValue) == typeof(int))
{
int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedValue);
CurrentValue = (TValue)(object)parsedValue;
}
else if (typeof(TValue) == typeof(int?))
{
if (value == null)
{
CurrentValue = (TValue)(object)null;
}
else
{
int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsedValue);
CurrentValue = (TValue)(object)parsedValue;
}
}
}
}
js:
window.select2Component = {
init: function (Id) {
//Initialize Select2 Elements
$('#' + Id).select2();
},
onChange: function (id, dotnetHelper, nameFunc) {
$('#' + id).on('select2:select', function (e) {
dotnetHelper.invokeMethodAsync(nameFunc, $('#' + id).val());
});
},
}