Validating Blazor sub-components? - asp.net-core

I had a Blazor component containing a persons name and their address. I have split out the address so I can reuse it. I am using 2 way data binding between the person and address to ensure that data is passed to the address and the person can receive address changes.
I cannot get validation to work though. The person full name and the Address line 1 cannot be blank. When I use VaidationSummary then it correctly reports that both fields cannot be blank. But when I use ValidationMessage only the person full name reports a validation message. I am using Fluent validation but I believe the issue is that ValidationMessage does not report when in a complex type.
I think it is because the For() attribute for the Address line 1 ValidationMessage does not match the field name in the master form (Person) data model. The master data model has the address class as Address but the address component has it as Value. However, if I am to reuse the component then this is likely to happen!
Separating components like addresses seems a reasonable thing to do and you might have more than one address object on a form (delivery and billing for example) so I just need to know how to do it.
Has anyone done this? Is a custom ValidationMessage needed or a different For() implementation?
Thanks for your help with this. Here is the source.
Form:
<EditForm Model=#FormData>
<FluentValidator/>
<ValidationSummary/>
<InputText #bind-Value=FormData.FullName />
<ValidationMessage For="#(() => FormData.FullName)"/>
<ComponentAddress #bind-Value=FormData.Address />
<input type="submit" value="Submit" class="btn btn-primary" />
</EditForm>
#code{
PersonDataModel FormData = new PersonDataModel();
}
Address Component:
<InputText #bind-Value=Value.Address1 #onchange="UpdateValue" />
<ValidationMessage For="#(() => Value.Address1)" />
#code{
[Parameter] public AddressDataModel Value { get; set; }
[Parameter] public EventCallback<AddressDataModel> ValueChanged { get; set; }
protected async Task UpdateValue()
{
await ValueChanged.InvokeAsync(Value);
}
}
Person model:
public class PersonDataModel
{
[Required]
public string FullName { get; set; }
public AddressDataModel Address { get; set; }
public PersonDataModel()
{
Address = new AddressDataModel();
}
}
Address model:
public class AddressDataModel
{
[Required]
public string Address1 { get; set; }
}
Person Fluent Validator:
public class PersonValidator : AbstractValidator<PersonDataModel>
{
public PersonValidator()
{
RuleFor(r => r.FullName).NotEmpty().WithMessage("You must enter a name");
RuleFor(r => r.Address.Address1).NotEmpty().WithMessage("You must enter Address line 1");
}
}

The problem is that the ValidationContext for validating your Component is your Component's Value property - not the model that the parent page is using.
I struggled to get something to get component validation working until I figured out a bit of a hack using another Validation Attribute that I apply to the Component's Value property. When validating Value, I use the Component's EditContext which is a property that is set via a Cascading Parameter. I can get the property's name via reflection and that means I can get the correct FieldIdentifier, notifiy that the field has changed and then get the ValidationResults from the parent's EditContext. I can then return the same error details.
Validation Attribute
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using ValueBinding.Shared;
namespace ValueBinding.Data.Annotations
{
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class MyValidationContextCheckAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
EditContext ec = null;
string propName = "NOT SET";
// My StringWrapper is basic component with manual Value binding
if (validationContext.ObjectInstance is MyStringWrapper)
{
var strComp = (MyStringWrapper)validationContext.ObjectInstance;
ec = strComp.ParentEditContext; // Uses Cascading Value/Property
propName = strComp.GetPropertyName();
}
if (ec != null)
{
FieldIdentifier fld = ec.Field(propName);
ec.NotifyFieldChanged(in fld);
// Validation handled in Validation Context of the correct field not the "Value" Property on component
var errors = ec.GetValidationMessages(fld);
if (errors.Any())
{
string errorMessage = errors.First();
return new ValidationResult(errorMessage, new List<string> { propName });
}
else
{
return null;
}
}
else if (typeof(ComponentBase).IsAssignableFrom(validationContext.ObjectType))
{
return new ValidationResult($"{validationContext.MemberName} - Validation Context is Component and not data class", new List<string> { validationContext.MemberName });
}
else
{
return null;
}
}
}
}
Component
#using System.Linq.Expressions
#using System.Reflection
#using Data
#using Data.Annotations
<div class="fld" style="border-color: blue;">
<h3>#GetPropertyName()</h3>
<InputText #bind-Value=#Value />
<ValidationMessage For=#ValidationProperty />
<div class="fld-info">#HelpText</div>
</div>
#code {
[Parameter]
public string Label { get; set; } = "NOT SET";
[Parameter]
public string HelpText { get; set; } = "NOT SET";
[Parameter]
public Expression<Func<string>> ValidationProperty { get; set; }
private string stringValue = "NOT SET";
[MyValidationContextCheck]
[Parameter]
public string Value
{
get => stringValue;
set
{
if (!stringValue.Equals(value))
{
stringValue = value;
_ = ValueChanged.InvokeAsync(stringValue);
}
}
}
[Parameter]
public EventCallback<string> ValueChanged { get; set; }
[CascadingParameter]
public EditContext ParentEditContext { get; set; }
public string GetPropertyName()
{
Expression body = ValidationProperty.Body;
MemberExpression memberExpression = body as MemberExpression;
if (memberExpression == null)
{
memberExpression = (MemberExpression)((UnaryExpression)body).Operand;
}
PropertyInfo propInfo = memberExpression.Member as PropertyInfo;
return propInfo.Name;
}
}

Related

How to set input box value to a string in Blazor WebAssembly?

I am using Blazor WebAssmebly. I have an input box and I simply want to reset it to no text after the user types in text and hits the Enter key:
<input id="txtWord" name="txtWord" placeholder="Enter your text" #onchange="#onChange" #onkeyup="#Enter" />
private void onChange(Microsoft.AspNetCore.Components.ChangeEventArgs args)
{
value = (string)args.Value;
}
public void Enter(KeyboardEventArgs e)
{
if (e.Code == "Enter" || e.Code == "NumpadEnter")
{
if (value.Trim() != "")
{
doSomething();
}
}
}
So I set the variable 'value' to the input text, but then I want to clear the text box. How do I do that?
It looks like as if you're not binding your input to the value variable. Your code should be something like this:
<input id="txtWord" name="txtWord" placeholder="Enter your text"
value ="#value" #onchange="onChange" #onkeyup="Enter" />
#code
{
private string value;
}
Note that by adding the value attribute to the input element I create a two-way databinding, from the variable to the control, and from the control to the variable. When you use the #onchange directive, you create a one-way data binding.
In order to reset the input element, you can do the following:
if (value.Trim() != "")
{
// I guess it is here that you want to reset the input
// element. Assigning empty string to the `value` variable
// will cause the control to re-render with the new value;
// that is empty string...
value = "";
doSomething();
}
This will handle "enter" and "Submit" button press. I use this in a SignalR library I am developing. The default css classes are for Bootstrap.
SendBox.razor
<EditForm Model="#SendBoxViewModel" OnSubmit="#Send">
<div class="#DivClass">
<input #ref="#inputBox"
#bind-value="SendBoxViewModel.InputMessage"
#bind-value:event="oninput"
type="text"
aria-label="#Placeholder"
placeholder="#Placeholder"
class="#InputClass"
aria-describedby="button-send"
disabled=#Disabled>
<button class="#ButtonClass"
type="submit"
id="button-send"
disabled=#Disabled>
#Label
</button>
</div>
</EditForm>
SendBox.razor.cs
public partial class SendBox : ComponentBase
{
private ElementReference inputBox;
[Parameter]
public string Label { get; set; } = "Send";
[Parameter]
public string Placeholder { get; set; } = "Type a new message here.";
[Parameter]
public string DivClass { get; set; } = "input-group";
[Parameter]
public string InputClass { get; set; } = "form-control";
[Parameter]
public string ButtonClass { get; set; } = "btn btn-outline-primary";
[Parameter]
public bool Disabled { get; set; }
[Parameter]
public EventCallback<string> OnSend { get; set; }
public SendBoxViewModel SendBoxViewModel { get; set; } = new SendBoxViewModel();
private bool MessageInputInvalid => string.IsNullOrWhiteSpace(SendBoxViewModel.InputMessage);
private async Task Send()
{
if (!MessageInputInvalid)
{
await OnSend.InvokeAsync(SendBoxViewModel.InputMessage);
SendBoxViewModel.InputMessage = string.Empty;
await inputBox.FocusAsync();
}
}
}
SendBoxViewModel.cs
public class SendBoxViewModel
{
[MinLength(length: 1)]
[MaxLength(length: 1024)]
[Required(AllowEmptyStrings = false)]
public string? InputMessage { get; set; }
}

Blazor's InputSelect Component not updating form validations errors on it's value change

I am using Blazor's InputSelect Component on a field called LocationId.
[Range(1, int.MaxValue, ErrorMessage = "Please Select Location")]
public int LocationId { get; set; }
On my razor component, where form validations are taking place, I am calling a child component like this:
<div class="form-group">
<label>Location</label>
<ValidationMessage For="#(() => StudentData.LocationId)" />
<SelectCommon RowType="Location" RowData="Locations" #bind-MyPhrase="#StudentData.LocationId">
<SelectOption>
<option selected disabled value="0">Choose a Location</option>
</SelectOption>
<OptionValue Context="p">
<option value="#p.Id">#p.City, #p.State</option>
</OptionValue>
</SelectCommon>
</div>
In the child component there is the InputSelect component whose code is:
#typeparam RowType
<InputSelect class="form-control" #bind-Value="HandleChange">
#if (SelectOption != null)
{
#SelectOption
}
#foreach (RowType item in RowData)
{
#OptionValue(item);
}
</InputSelect>
#code {
[Parameter]
public RenderFragment SelectOption { get; set; }
[Parameter]
public RenderFragment<RowType> OptionValue { get; set; }
[Parameter]
public IEnumerable<RowType> RowData { get; set; }
[Parameter]
public int MyPhrase { get; set; }
[Parameter]
public EventCallback<int> MyPhraseChanged { get; set; }
public int HandleChange
{
get { return MyPhrase; }
set
{
MyPhrase = value;
MyPhraseChanged.InvokeAsync(MyPhrase);
}
}
}
The work of #bind-Value="HandleChange" is to create blazor chain binding thing which is working perfectly. The parent component has this attribute #bind-MyPhrase="#StudentData.LocationId" which send the value of the model to the child for binding.
The problem is happening when i change the value of the select but the validation messages are not updated. However when I click the button which submits the form the validation messages updates. You can see the below gif which is showning this thing.
I have also notices that If id do not go with the chain binding approach and keep my InputSelect directly inside the EditForm component then this problem does not happen. It happens only in the parent-child way of coding (chain binding).
How can i correct this things?
Thanks to #dani-herrera
ApplicationUserDropDown.razor (child component):
<TelerikComboBox id="userbox" #bind-Value="#Value"
Data="#Data" TItem="UserListVM"
TValue="Guid?" Placeholder="Select User"
ValueField="Id" TextField="FullName">
ApplicationUserDropDown.razor.cs (child component):
public partial class ApplicationUserDropDown
{
#region Value two way binding
[CascadingParameter] EditContext EditContext { get; set; } = default!;
[Parameter] public Expression<Func<Guid?>> ValueExpression { get; set; }
private Guid? _valueId;
[Parameter]
public Guid? Value
{
get => _valueId;
set
{
if (_valueId == value) return;
_valueId = value;
ValueChanged.InvokeAsync(value);
var fieldIdentifier = FieldIdentifier.Create(ValueExpression);
EditContext.NotifyFieldChanged(fieldIdentifier);
}
}
[Parameter]
public EventCallback<Guid?> ValueChanged { get; set; }
#endregion Value two way binding
razor page using child component:
<ApplicationUserDropDown #bind-Value="Account.OwnerId"></ApplicationUserDropDown>
<ValidationMessage For="#(() => Account.OwnerId)">Owner is Required</ValidationMessage>

Razor Components (Blazor) - render-mode affecting lists of derived classes

I've got a Razor Component i'm dropping onto a Razor page like so:
<component type="typeof(ElementProductComponent)"
render-mode="Server"
param-elementList="Model.elementList" />
I'm passing a list (elementList) into the component as a parameter.
The list is declared in the razor page code behind as
[BindProperty]
public List<ElementModel> elementList { get; set; }
ElementModel is a base class. The list will normally contain items of various types derived from the base class (ElementTextModel, ElementDateModel, etc)
In the codebind for the razor page the list is populated like this:
public async Task<IActionResult> OnGet(string id)
{
elementList = GetList();
return Page();
}
public static List<ElementModel> GetList()
{
var elements = new List<ElementModel>();
elements.Add(new ElementTextModel { Name = "TestTextName", Value = "hello world" });
elements.Add(new ElementDateModel { Name = "TestDateName", Value = "20200101" });
return elements;
}
The models look like this:
public class ElementModel
{
[Required]
public string Name { get; set; }
[Required]
public virtual ElementType Type { get; }
}
public class ElementTextModel : ElementModel
{
[Required]
public string Value { get; set; }
public Hashtable Attributes { get; set; }
[Required]
public override ElementType Type { get { return ElementType.TEXT; } }
}
Then in my component, I want to iterate over the list and display different html, child components based on the type of each item in the list. I can do that that with a switch or if. Here is a simplified version of the code where I'm just trying to display the type I see.
<div>
Elements:
<ul>
#for (var i = 0; i < elementList.Count; i++)
{
<li>
<div>#elementList[i].Type</div>
<div>#elementList[i].Name</div>
</li>
}
</ul>
</div>
#code {
[Parameter]
public List<ElementModel> elementList { get; set; }
}
The issue is that when I declare the component like
<component type="typeof(ElementProductComponent)"
render-mode="Server"
param-elementList="Model.elementList" />
with the render-mode set to Server, the code in the component sees the items in the elementList as of type ElementModel, regardless of how they were declared. This isn't correct.
if I do this:
<component type="typeof(ElementProductComponent)"
render-mode="Static"
param-elementList="Model.elementList" />
and set the render-mode to Static, the component sees the list correctly, as a mixed list of items of types derived from the base class.
Why is this happening? Is this a bug or is there some configuration issue at play?

Encrypted Id is not retaining back in controller

I am encrypting id to hide the raw id in query string and passing it to the controller. But the Id is not retaining back in the postback to the controller.
for eg
/Vendor/EditVendor/mELirpUhRYksFj7k8-XBcQ%3d%3d
DecryptLong() method will decrypt the above id string mELirpUhRYksFj7k8-XBcQ%3d%3d to 1
controller
public ActionResult EditVendor(string id)
{
var vendor = _vendorService.GetVendorById(id.DecryptLong());
return View(vendor);
}
[HttpPost]
public ActionResult EditVendor(Vendor vendor)
{
if (ModelState.IsValid)
{
vendor.Id -- it is always zero and not retaining back
_vendorService.EditVendor(vendor);
}
return View(vendor);
}
In view
#model Eclatech.KidsHub.Objects.Vendor
#{
ViewBag.Title = "EditVendor";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>Edit Vendor</h2>
#using(Html.BeginForm("EditVendor","Vendor",FormMethod.Post, new Dictionary<string, object>
{
{"class","form-horizontal"},
{"role","form"}
}))
{
<div class="form-group">
#Html.LabelFor(m => m.VendorName, new Dictionary<string, object>
{
{"class","col-sm-2 control-label"}
})
<div class="col-sm-10">
#Html.TextBoxFor(m => m.VendorName,new Dictionary<string, object>
{
{"class","form-control"}
})
</div>
</div>
#Html.HiddenFor(m => m.Id)
<input type="submit" class="btn btn-primary btn-default" value="Save" />
}
Model
public class Vendor : AuditableEntity<long>
{
public string VendorName { get; set; }
public virtual ICollection<Product> Products { get; set; }
}
public abstract class AuditableEntity<T> : Entity<T>, IAuditableEntity
{
[ScaffoldColumn(false)]
public DateTime CreatedDate { get; set; }
[MaxLength(256)]
[ScaffoldColumn(false)]
public string CreatedBy { get; set; }
[ScaffoldColumn(false)]
public DateTime UpdatedDate { get; set; }
[MaxLength(256)]
[ScaffoldColumn(false)]
public string UpdatedBy { get; set; }
}
public abstract class Entity<T> : BaseEntity, IEntity<T>
{
private static long _rowNumber;
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public virtual T Id { get; set; }
[NotMapped]
public virtual long RowNumber
{
get { return ++_rowNumber; }
}
}
The problem is that your parameter name for the EditVendor method is named id and you are returning a model that also has a property named id. When you call the EditVendor method, the value of the parameter is added to ModelState which overrides the value of property Vendor.Id. If you inspect the html generated by #Html.HiddenFor(m => m.Id) you will see that the value of the input is mELirpUhRYksFj7k8-XBcQ%3d%3d, not the value returned by DecryptLong(). When this posts back, it cannot be bound to type int so Id has its default value of zero.
You can test this by adding ModelState.Clear(); before calling GetVendorById(). This will clear the value of Id and the hidden inputs value will now be 1. To solve the problem, change the name of the parameter, for example
public ActionResult EditVendor(string vendorID)
{
var vendor = _vendorService.GetVendorById(vendorID.DecryptLong());
return View(vendor);
}

What is the name convention of binding a complex ViewModels?

I have these two ViewModels
public class AboutViewModel : ViewModel
{
public override long Id { get; set; }
public override string PageTitle { get; set; }
public override string TitleDescription { get; set; }
public override string ContentTitle { get; set; }
public virtual AboutItemViewModel AboutItem { get; set; }
}
public class AboutItemViewModel
{
public long Id { get; set; }
[AllowHtml]
public string Content { get; set; }
public string ImageUrl { get; set; }
public HttpPostedFileBase FileToUpload { get; set; }
}
Here is my controller:
[ValidateInput(false)]
[ValidateAntiForgeryToken, HttpPost]
public ActionResult Create(long? siteid, long? cid, AboutViewModel model)
{
return View(model);
}
Here is my View:
#using (Html.BeginForm("Create", "About", new { siteid = ViewData["siteid"], cid = ViewData["cid"] },FormMethod.Post,new { enctype = "multipart/form-data", #class = "form-horizontal rtl", autocomplete = "off" }))
{
<div class="controls">
<input type="file" name="FileToUpload" id="FileToUpload" style="margin-right: -9px;">
</div>
<div class="controls">
#Html.ValidationMessageFor(o => o.AboutItem.FileToUpload, "", new { id = "spanfile", #class = "alert alert-block alert-error span3 pull-right", style = "margin-right: 160px;" })
</div>
<div class="control-group pull-left">
<button type="submit" class="btn btn-large" data-toggle="button">Save</button>
</div>
}
How to bind the file to FileToUpload to stop returning me a null?
Except:
If I put it in the main AboutViewModel than it's returns a correct value.
Since the FileToUpload property is in the AboutItem proprety, which is a class property of the parent ViewModel, you need to preface the name of your input element with the property it came from. That's a long way of saying that the name of your file input should be AboutItem.FileToUpload.
<input type="file" name="AboutItem.FileToUpload" id="AboutItem_FileToUpload" />
This should take care of the model binding for you. Additionally, you can test this by using an HTML helper on on of the other properties of the AboutItem class. For instance:
#Html.TextBoxFor(x=>x.AboutItem.Id)
This should render in the HTML
<input type="text" name="AboutItem.Id" id="AboutItem_Id />
EDIT
Apparently the id attribute will be rendered with an underscore instead of a dot. However, since the ID attribute is not used in model binding, it shouldn't really matter.