Custom Blazor DateTime using inherits InputBase<DateTime> with ValidationMessage - c#

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

Related

Date validator in Blazor Server Side

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

Blazor Custom Multiselect Dropdown Component Webassembly

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.

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.

Blazor implementation of Bootstrap Vue B-Checkbox with array model

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();
}

2 models in 1 view MVC4

I have two models as shown below:
One:
[Validator(typeof(BlogPostValidator))]
public partial class BlogPostModel : BaseNopEntityModel
{
public BlogPostModel()
{
Tags = new List<string>();
Comments = new List<BlogCommentModel>();
AddNewComment = new AddBlogCommentModel();
}
public string SeName { get; set; }
public string Title { get; set; }
public string Body { get; set; }
public bool AllowComments { get; set; }
public int NumberOfComments { get; set; }
public DateTime CreatedOn { get; set; }
public IList<string> Tags { get; set; }
public IList<BlogCommentModel> Comments { get; set; }
public AddBlogCommentModel AddNewComment { get; set; }
public BlogCommentModel blogcommentmodel { get; set; }
}
two:
public partial class BlogCommentModel : BaseNopEntityModel
{
public int CustomerId { get; set; }
public string CustomerName { get; set; }
public string CustomerAvatarUrl { get; set; }
public string CommentText { get; set; }
public DateTime CreatedOn { get; set; }
public bool AllowViewingProfiles { get; set; }
public int CommentParentID { get; set; }
public IList<BlogComment> ChildCommentList { get; set; }//netra
}
I want to make use of ChildCommentList from BlogCommentModel to show nested comments.
My View:
#model BlogPostModel
#using Nop.Web.Models.Blogs;
#if (Model.AllowComments)
{
<div class="clear">
</div>
<fieldset class="new-comment" id="addcomment">
<legend class="title">#T("Blog.Comments.LeaveYourComment")</legend>
#using (Html.BeginForm())
{
<div>
<div class="message-error">#Html.ValidationSummary(true)</div>
#{
string result = TempData["nop.blog.addcomment.result"] as string;
}
#if (!String.IsNullOrEmpty(result))
{
<div class="result">#result</div>
}
<div class="forms-box">
<div class="inputs">
#Html.LabelFor(model => model.AddNewComment.CommentText)
<div class="input-box">
#Html.TextAreaFor(model => model.AddNewComment.CommentText, new { #class = "comment-text" })
</div>
#Html.ValidationMessageFor(model => model.AddNewComment.CommentText)
</div>
#if (Model.AddNewComment.DisplayCaptcha)
{
<div class="captcha-box">
#Html.Raw(Html.GenerateCaptcha())
</div>
<div class="clear">
</div>
}
</div>
<div class="clear">
</div>
<div class="buttons">
<input type="submit" name="add-comment" class="button-1 blog-post-add-comment-button" value="#T("Blog.Comments.SubmitButton")" />
</div>
</div>
}
</fieldset>
if (Model.Comments.Count > 0)
{
<div class="clear">
</div>
<div class="comment-list">
<div class="title">
#T("Blog.Comments")
</div>
<div class="clear">
</div>
#foreach (var comment in Model.Comments)
{
<div class="blog-comment">
<div class="comment-info">
<div class="user-info">
#if (comment.AllowViewingProfiles)
{
#(comment.CustomerName)
}
else
{
<span class="username">#(comment.CustomerName)</span>
}
<div class="avatar">
#if (!String.IsNullOrEmpty(comment.CustomerAvatarUrl))
{
<img src="#(comment.CustomerAvatarUrl)" class="avatar-img" title="avatar" alt="avatar" />
}
</div>
</div>
</div>
<div class="comment-content">
<div class="comment-time">
#T("Blog.Comments.CreatedOn"): <span class="stat-value">#comment.CreatedOn.ToString("g")</span>
</div>
<div class="comment-body">
#Html.Raw(Nop.Core.Html.HtmlHelper.FormatText(comment.CommentText, false, true, false, false, false, false))
</div>
</div>
#Html.Widget("blogpost_page_inside_comment")
</div>
<div class="clear">
</div>
<div>
#foreach(var childcomments in Model.blogcommentmodel.ChildCommentList)
{
#Html.Raw(Nop.Core.Html.HtmlHelper.FormatText(childcomments.CommentText, false, true, false, false, false, false))
}
</div>
}
<div class="buttons">
<input type="submit" id="replyto" name="reply-comment" class="button-1 blog-post-add-comment-button" value="#T("Blog.Comments.ReplyButton")" />
</div>
</div>
}
}
Error occurred on :
#foreach(var childcomments in Model.blogcommentmodel.ChildCommentList)
{
#Html.Raw(Nop.Core.Html.HtmlHelper.FormatText(childcomments.CommentText, false, true, false, false, false, false))
}
</div>
}
NullReferenceException was unhandled by usercode -{"Object reference not set to an instance of an object."}
My controller code for ActionResult:
public ActionResult BlogPost(int blogPostId)
{
if (!_blogSettings.Enabled)
return RedirectToRoute("HomePage");
var blogPost = _blogService.GetBlogPostById(blogPostId);
if (blogPost == null ||
(blogPost.StartDateUtc.HasValue && blogPost.StartDateUtc.Value >= DateTime.UtcNow) ||
(blogPost.EndDateUtc.HasValue && blogPost.EndDateUtc.Value <= DateTime.UtcNow))
return RedirectToRoute("HomePage");
var model = new BlogPostModel();
**PrepareBlogPostModel(model, blogPost, true);**
return View(model);
}
Below method called in above ActionResult:
[NonAction]
protected void PrepareBlogPostModel(BlogPostModel model, BlogPost blogPost, bool prepareComments)
{
if (blogPost == null)
throw new ArgumentNullException("blogPost");
if (model == null)
throw new ArgumentNullException("model");
model.Id = blogPost.Id;
model.SeName = blogPost.GetSeName();
model.Title = blogPost.Title;
model.Body = blogPost.Body;
model.AllowComments = blogPost.AllowComments;
model.CreatedOn = _dateTimeHelper.ConvertToUserTime(blogPost.CreatedOnUtc, DateTimeKind.Utc);
model.Tags = blogPost.ParseTags().ToList();
model.NumberOfComments = blogPost.ApprovedCommentCount;
model.AddNewComment.DisplayCaptcha = _captchaSettings.Enabled && _captchaSettings.ShowOnBlogCommentPage;
if (prepareComments)
{
// var blogchildcomment = _blogService.GetAllChildComments();
var blogComments = blogPost.BlogComments.Where(pr => pr.IsApproved).OrderBy(pr => pr.CreatedOnUtc);
foreach (var bc in blogComments)
{
var commentModel = new BlogCommentModel()
{
Id = bc.Id,
CustomerId = bc.CustomerId,
CustomerName = bc.Customer.FormatUserName(),
CommentText = bc.CommentText,
CreatedOn = _dateTimeHelper.ConvertToUserTime(bc.CreatedOnUtc, DateTimeKind.Utc),
AllowViewingProfiles = _customerSettings.AllowViewingProfiles && bc.Customer != null && !bc.Customer.IsGuest(),
CommentParentID = bc.CommentParentID,//Netra
};
//Netra
var comments = _blogService.GetBlogComments(bc.CommentParentID);
if (comments != null)
{
commentModel.ChildCommentList = new List<BlogComment>();
foreach (var cmnt in comments)
{
commentModel.ChildCommentList.Add(cmnt);
}
}
if (_customerSettings.AllowCustomersToUploadAvatars)
{
var customer = bc.Customer;
string avatarUrl = _pictureService.GetPictureUrl(customer.GetAttribute<int>(SystemCustomerAttributeNames.AvatarPictureId), _mediaSettings.AvatarPictureSize, false);
if (String.IsNullOrEmpty(avatarUrl) && _customerSettings.DefaultAvatarEnabled)
avatarUrl = _pictureService.GetDefaultPictureUrl(_mediaSettings.AvatarPictureSize, PictureType.Avatar);
commentModel.CustomerAvatarUrl = avatarUrl;
}
model.Comments.Add(commentModel);
}
}
}
BlogComment is class which describes the properties.
public partial class BlogComment : CustomerContent
{
/// <summary>
/// Gets or sets the comment text
/// </summary>
public virtual string CommentText { get; set; }
/// <summary>
/// Gets or sets the blog post identifier
/// </summary>
public virtual int BlogPostId { get; set; }
/// <summary>
/// Gets or sets the blog post
/// </summary>
public virtual BlogPost BlogPost { get; set; }
public virtual int CommentParentID { get; set; } //netra
}
So you have a null somewhere - you need to find out what's null and make it so it's not null; or change the Razor code to something like:
#if(Model.blogcommentmodel != null && Model.blogcommentmodel.ChildCommentList != null)
{
#* original #foreach statement here *#
}
My first guess would be that you need to create a default constructor for your model. In the constructor, set ChildCommentList = new List<BlogComment>();.
It looks like you didn't set that in your Controller, which the way that you have your Model set up, would cause ChildCommentList to still be null. With that default constructor, you won't have to worry about your code breaking like this if you miss setting it somewhere.
To answer wnetra's comment, the constructor is on the BlogcommentModel class. So you'll need something like this:
public class BlogcommentModel {
/* All of the property declarations */
public IList<BlogComment> ChildCommentList { get; set; }
/* Constructor for the BlogcommentModel class */
public BlogcommentModel() {
ChildcommentList = new List<BlogComment>();
}
}
Any reference objects should always be instantiated inside a default constructor to ensure they won't be null when trying to reference them in code.
Also see Andras' answer with regard to always doing != null when trying to reference reference objects in your code.
I have removed following code from BlogCommenmodel :
//public int CommentParentID { get; set; }
//public IList<BlogComment> ChildCommentList { get; set; }//netra
//public BlogCommentModel()
//{
// ChildCommentList = new List<BlogComment>();
//}
and introduced in BlogPostModel
[Validator(typeof(BlogPostValidator))]
public partial class BlogPostModel : BaseNopEntityModel
{
public BlogPostModel()
{
Tags = new List<string>();
Comments = new List<BlogCommentModel>();
AddNewComment = new AddBlogCommentModel();
ChildCommentList = new List<BlogComment>();
}
public string SeName { get; set; }
public string Title { get; set; }
public string Body { get; set; }
public bool AllowComments { get; set; }
public int NumberOfComments { get; set; }
public DateTime CreatedOn { get; set; }
public IList<string> Tags { get; set; }
public IList<BlogCommentModel> Comments { get; set; }
public AddBlogCommentModel AddNewComment { get; set; }
//Netra
public int CommentParentID { get; set; }
public IList<BlogComment> ChildCommentList { get; set; }//netra
// public BlogCommentModel blogcommentmodel { get; set; }
}
IN View:
<div>
#if (Model.ChildCommentList != null)
{
foreach (var childcomments in Model.ChildCommentList)
{
#Html.Raw(Nop.Core.Html.HtmlHelper.FormatText(childcomments.CommentText, false, true, false, false, false, false))
}
}
</div>

Categories

Resources