Get full HTML field name for client side validation in ASP.NET Core - asp.net-core

I'm implementing a custom validation attribute. This attribute does not only look at the value of the property it is applied to, but also at the value of another property. The other property is specified by its name.
I need to find a way to get the full id that the input for the other property will have in the final HTML output.
This is a simplified version of my validation attribute:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public class MyCustomValidationAttribute : ValidationAttribute, IClientModelValidator
{
private string _otherPropertyName;
public MyCustomValidationAttribute(string otherPropertyName)
{
_otherPropertyName = otherPropertyName;
}
protected override ValidationResult IsValid(object value, ValidationContext context)
{
var otherProperty = context.ObjectInstance.GetType().GetProperty(_otherPropertyName);
var otherPropertyValue = Convert.ToString(otherProperty.GetValue(context.ObjectInstance, null));
// Validation logic...
}
public void AddValidation(ClientModelValidationContext context)
{
MergeAttribute(context.Attributes, "data-val", "true");
var errorMessage = FormatErrorMessage(context.ModelMetadata.GetDisplayName());
MergeAttribute(context.Attributes, "data-val-mycustomvalidation", errorMessage);
// THIS ROW NEEDS TO BE FIXED
MergeAttribute(context.Attributes, "data-val-mycustomvalidation-otherpropertyname", _otherProperyName);
}
private void MergeAttribute(IDictionary<string, string> attributes, string key, string value)
{
if (!attributes.ContainsKey(key))
{
attributes.Add(key, value);
}
}
}
This demonstrates how it is used in a model class:
public class Report
{
[MyCustomValidation("Value2", ErrorMessage = "Error...")]
public string Value1 { get; set; }
public string Value2 { get; set; }
}
This is the JavaScript to make sure that the validation is also done on the client side:
$.validator.addMethod('mycustomvalidation',
function (value, element, parameters) {
var otherPropertyValue = $('#' + parameters.otherpropertyname).val();
// Validation logic...
});
$.validator.unobtrusive.adapters.add('mycustomvalidation', ['otherpropertyname'],
function (options) {
options.rules.mycustomvalidation = options.params;
options.messages['mycustomvalidation'] = options.message;
});
My viewmodel for the page/view with the form looks like this:
public MyViewModel
{
public Report MyReport { get; set; }
}
Note that I'm not using Report as my viewmodel, but rather as the type of a property in the viewmodel. This is important since this is the root of my problem...
The code in the view to show the input for Value1 is nothing strange (I'm using Razor Pages):
<div>
<label asp-for="MyReport.Value1"></label>
<input asp-for="MyReport.Value1" />
<span asp-validation-for="MyReport.Value1"></span>
</div>
And the output becomes:
<label for="MyReport_Value1">Value1</label>
<input
type="text"
id="MyReport_Value1"
name="MyReport.Value1"
data-val="true"
data-val-mycustomvalidation="Error..."
data-val-mycustomvalidation-otherpropertyname="Value2"
value=""
>
<span
data-valmsg-for="MyReport.Value1"
data-valmsg-replace="true"
class="text-danger field-validation-valid"
></span>
So the problem is that in the HTML output I need data-val-mycustomvalidation-otherpropertyname to be "MyReport_Value2" instead of just "Value2". Otherwise the validation code won't be able to find the second HTML input (with id MyReport_Value2) and perform the validation.
I figure this must be done in the method AddValidation() in the attribute class, but how do I get the full name that the HTML input will recieve?
I'm guessing there is some way to get this by using the context parameter. I've seen examples of something like "*.TemplateInfo.GetFullHtmlFieldId(PropertyName)" but I can't get it to work.
Any help is appreciated!

You pass Value2 to MyCustomValidationAttribute and set _otherPropertyName with Value2,and use
MergeAttribute(context.Attributes, "data-val-mycustomvalidation-otherpropertyname", _otherProperyName);
so that html will be
data-val-mycustomvalidation-otherpropertyname="Value2"
You only need to pass Report_Value2 to MyCustomValidationAttribute rather than Value2.
public class Report
{
[MyCustomValidation("Report_Value2", ErrorMessage = "Error...")]
public string Value1 { get; set; }
public string Value2 { get; set; }
}
So that you will get
data-val-mycustomvalidation-otherpropertyname="Report_Value2"

ValidationContext is binded to instance that belong to validating property i.e Model. Hence locating reference of ViewModel looks difficult.
I can provide three different solution you can use which one suits your requirement.
Solution 1:
Using ValidationContext you can able to get Name of the class where Property belong to. This solution will work only if ViewModel Property Name must be same as Model Class Name.
e.g. if Model Class is Student then property name must be Student. If property name is Student1 it wont work.
Solution 2 & 3 will work even if Class name and property name are different.
Model
public class Student
{
[Key]
public int Id { get; set; }
[Required(ErrorMessage = "Please enter name")]
public string Name { get; set; }
[Required]
[Country("Name")]
public string Country { get; set; }
}
ViewModel
public class StudentViewModel
{
public Student Student {get;set;} //Solution 1 wil not work for Student1
}
ValidationAttribute
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public class CountryAttribute : ValidationAttribute, IClientModelValidator
{
private string _otherPropertyName;
private string _clientPropertyName;
public CountryAttribute(string otherPropertyName)
{
_otherPropertyName = otherPropertyName;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var otherProperty = validationContext.ObjectInstance.GetType().GetProperty(_otherPropertyName);
var otherPropertyValue = Convert.ToString(otherProperty.GetValue(validationContext.ObjectInstance, null));
_clientPropertyName = otherProperty.DeclaringType.Name +"_"+ otherProperty.Name;
}
public void AddValidation(ClientModelValidationContext context)
{
context.Attributes.Add("data-val", "true");
context.Attributes.Add("data-val-mycustomvalidation-otherpropertyname", _clientPropertyName);
}
}
Solution 2:
Using ClientModelValidationContext you can able to get ViewModel reference that is passed from the controller to view. By using reflection we can get the name of the property i.e Model.
To work with solution you need to pass empty ViewModel reference from controller.
Controller
public IActionResult New()
{
StudentViewModel studentViewModel = new StudentViewModel();
return View(studentViewModel);
}
ValidationAttribute
public void AddValidation(ClientModelValidationContext context)
{
var otherClientPropName = context.ModelMetadata.ContainerMetadata.Properties
.Single(p => p.PropertyName == this._otherPropertyName)
.GetDisplayName();
var viewContext = context.ActionContext as ViewContext;
if (viewContext?.ViewData.Model is StudentViewModel)
{
var model = (StudentViewModel)viewContext?.ViewData.Model;
var instanceName = model.GetType().GetProperties()[0].Name;
otherClientPropName = instanceName + "_" + otherClientPropName;
}
context.Attributes.Add("data-val", "true");
context.Attributes.Add("data-val-mycustomvalidation-otherpropertyname", otherClientPropName);
}
Solution 3:
Using context.Attributes["id"] you can able to get current property id value as string . By using string manipulation you can get prefix then you can merge with other property name.
This solution doesn't require empty ViewModel reference from controller.
Controller
public IActionResult New()
{
return View();
}
ValidationAttribute
public void AddValidation(ClientModelValidationContext context)
{
var otherClientPropName = context.ModelMetadata.ContainerMetadata.Properties
.Single(p => p.PropertyName == this._otherPropertyName)
.GetDisplayName();
var id = context.Attributes["id"];
var idPrefix = id.Split("_");
if (idPrefix.Length > 1)
{
otherClientPropName = idPrefix[0] + "_" + otherClientPropName;
}
context.Attributes.Add("data-val", "true");
context.Attributes.Add("data-val-mycustomvalidation-otherpropertyname", otherClientPropName);
}
HTML Output
<input class="form-control" type="text" data-val="true" data-val-required="Please enter name" id="Student_Name" name="Student.Name" value="">
<input class="form-control input-validation-error" type="text" data-val="true" data-val-mycustomvalidation-otherpropertyname="Student_Name" data-val-required="The Country field is required." id="Student_Country" name="Student.Country" value="">

This is a method that also works when there are fields rendered that are deeper children of the model.
//Build the client id of the property name.
var dependentClientId = dependentPropertyName;
var clientId = context.Attributes["id"];
var clientIdArr = clientId.Split("_");
if (clientIdArr.Length > 1)
{
//Replace the last value of the array with the dependent property name.
clientIdArr[clientIdArr.Length - 1] = dependentPropertyName;
dependentClientId = string.Join("_", clientIdArr);
}
MergeAttribute(context.Attributes, "data-val-mycustomvalidation-otherpropertyname", dependentClientId );

Related

PageRemote attribute doesn't send __RequestVerificationToken

I'm using .NET 6 and razor pages.
[PageRemote] attribute in POST method doesn't send __requestverificationtoken to server and I get error 400.
This is my ViewModel
public class AddCategory
{
[PageRemote(PageName = "Category", PageHandler = "CheckForTitle",
HttpMethod = "POST",
AdditionalFields = "__RequestVerificationToken",
ErrorMessage = "This title is duplicate")]
public string Title { get; set; } = null!;
}
And this is my handler
public class CategoryModel : PageModel
{
[BindProperty]
public AddCategory Category { get; set; }
public void OnGet()
{
}
public IActionResult OnPostCheckForTitle(AddCategory category)
{
return new JsonResult(category.Title == "a");
}
}
GET method is ok and everything is fine, but in POST method __requestverificationtoken doesn't send to the server and I get error 400.
The property that you are trying to validate is on a nested property. All fields listed in the AdditionalFields property will be prefixed with the nested property name when they are posted, so the request verification token will be posted as Category.__RequestVerificationToken. As a result, the request verification token itself is not found, and request verification fails resulting in the 400 status code.
You should add a separate string property to the PageModel, Title, then apply the PageRemote attribute to that and reference it in the input tag helper via asp-for. Once you are happy that the submission is valid, you can assign the posted Title value to the relevant property in your Category object and process as usual.
public class CategoryModel : PageModel
{
[BindProperty]
public AddCategory Category { get; set; }
[BindProperty, PageRemote(PageName = "Category", PageHandler = "CheckForTitle",
HttpMethod = "POST",
AdditionalFields = "__RequestVerificationToken",
ErrorMessage = "This title is duplicate")]
public string Title { get; set; } = null!;
public IActionResult OnPostCheckForTitle(AddCategory category)
{
return new JsonResult(Title == "a");
}
}
#Mike Brind has been explained very nice. One way you can split the property from model and use asp-for="PropertyName" instead of nested property.
Another way is just override the name by specifying the name attribute like below:
<form method="post">>
//if you use method="post", it will auto generate token
//if you do not use method="post", remember add token like below
#*#Html.AntiForgeryToken()*#
<div class="form-group">
<label asp-for="Category.Title"></label>
//add the name....
<input asp-for="Category.Title" name="Title" class="form-control" />
<span asp-validation-for="Category.Title" class="text-danger"></span>
</div>
</form>
#section Scripts
{
<partial name="_ValidationScriptsPartial" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-ajax-unobtrusive/3.2.6/jquery.unobtrusive-ajax.js" integrity="sha256-v2nySZafnswY87um3ymbg7p9f766IQspC5oqaqZVX2c=" crossorigin="anonymous"></script>
}

Razor Pages Custom Tag Helper 2 Way Bind

I am wanting to create a custom tag helper in razor pages which binds to a custom model but the value is not being read back into the modal on post, below is my TagHelper code
[HtmlTargetElement("kenai-date", TagStructure = TagStructure.WithoutEndTag)]
public class Date : TagHelper
{
//public string Value { get; set; }
public ModelExpression AspFor { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "input";
output.TagMode = TagMode.SelfClosing;
output.Attributes.Add("value", this.AspFor.Model);
}
}
I am using the TagHelper with the below code
<kenai-date asp-for="DateValue" />
'DateValue' is a public property on the page, when first rendering the page the value of DateValue is correctly visible in the TagHelper Input element, if I force an OnPost, the value is removed.
I have applied the same to a standard input element with asp-for set and that works fine so suspect I am missing something in my TagHelper.
Asp.net core bind model data with name attribute.You use a custom tag helper,so it will get html like <input value="xxx">.So when form post,you cannot bind model data with name attribute,and when return Page in OnPost handler,model data is null.You need to add name attribute to <kenai-date asp-for="DateValue" />.Here is a demo:
TestCustomTagHelper.cshtml:
<form method="post">
<kenai-date asp-for="DateValue" name="DateValue" />
<input type="submit" />
</form>
TestCustomTagHelper.cshtml.cs:
public class TestCustomTagHelperModel : PageModel
{
[BindProperty]
public string DateValue { get; set; }
public void OnGet()
{
DateValue = "sss";
}
public IActionResult OnPost()
{
return Page();
}
}
result:

Microsoft.AspNetCore.Components.Forms.InputRadioGroup` does not support the type xxx

I want to use radio group in blazor so after implementing edit form and select one of the radio button I got this error :
Microsoft.AspNetCore.Components.Forms.InputRadioGroup`1[EGameCafe.SPA.Models.GameModel] does not support the type 'EGameCafe.SPA.Models.GameModel'.
here is my edit form :
<EditForm Model="ViewModel" OnValidSubmit="HandleCreateGroup">
#if (ViewModel.Games.List.Any())
{
<InputRadioGroup Name="GameSelect" #bind-Value="Gamemodelsample">
#foreach (var game in ViewModel.Games.List)
{
<InputRadio Value="game" />
#game.GameName
<br />
}
</InputRadioGroup>
}
</EditForm>
#code{
public GameModel GameModelSample { get; set; } = new();
}
and GameModel is :
public class GameModel
{
public string GameId { get; set; }
public string GameName { get; set; }
}
The InputRadioGroup, like other Blazor components, supports only a limited amount of types like String or Int32. You had the right idea, but unfortunately, you run into a kind of limitation of Blazor.
You could try to create a wrapper field.
private String _selectedGameId = "<Your Default Id>";
public String SelectedGameId
{
get => _selectedGameId;
set
{
_selectedGameId = value;
// Set the property of the ViewModel used in your Model Property of the EditContext or any other property/field
ViewModel.SelectedGame = ViewModel.Games.List?.FirstOrDefault(x => x.GameId == value);
}
}
Use the property SelectedGameId as the bind value of the InputRadioGroup component.
<InputRadioGroup Name="GameSelect" #bind-Value="SelectedGameId" >
#foreach (var game in ViewModel.Games.List)
{
<InputRadio Value="game.GameId" />
#game.GameName
<br />
}
</InputRadioGroup>
As an alternative, you can create a custom component that inheriting from InputRadioGroup to create a kind of GameBasedInputRadioGroup. If you are interested I can post a sample.
Because in your code #bind-Value="Gamemodelsample",you are trying to bind GameName(string) to Gamemodelsaple(object), which will cause type mismatch problems.
You only need to modify your code to:
#bind-Value="GameModelSample.GameName"

How to generate a Razor Page url within a custom TagHelper

I have a custom tag helper which should render something like this:
<ol>
<li>Some text
</ol>
If I were to do this within a Razor Page I would simply do something like this: <a asp-page="MyRazorPage">Some text</a>
Is there a way to do something similar inside of the TagHelper?
I found the answer.
Inject IUrlHelperFactory into the constructor as well as use the following property:
[HtmlAttributeNotBound]
[ViewContext]
public ViewContext ViewContext { get; set; }
Then you can create an IUrlHelper this way:
var urlHelper = _urlHelperFactory.GetUrlHelper(ViewContext);
var url = urlHelper.Page("/Clients/Edit", new { Id = myClientId });
output.Content.AppendHtmlLine($"<a href='{url}'>Edit</a>");
TagHelper provides HtmlTargetElement to add attributes to specified tags. Take adding asp-cuspage to the tag <a> as an example. The method Init is used to receive the parameters in the instruction asp-cuspage="". This method Process provides output attributes.
Create class CusAnchorTagHelper:
[HtmlTargetElement("a")]
public class CusAnchorTagHelper : TagHelper
{
private const string CuspageAttributeName = "asp-cuspage";
[HtmlAttributeName(CuspageAttributeName)]
public string Cuspage { get; set; }
public string Value { get; set; }
public override void Init(TagHelperContext context)
{
if (context.AllAttributes[0].Value != null)
{
Value = context.AllAttributes[0].Value.ToString();
}
base.Init(context);
}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
var reg = new Regex("(?<!^)(?=[A-Z])");
string attr="";
foreach(var a in reg.Split(Value))
{
attr += a + "/";
}
output.Attributes.SetAttribute("href", attr);
}
}
Then, inject custom taghelper assembly into the page. And it will be drawn in the view.
This is the rendered result.

ASP.NET Core- Is there a way to render group of elements each using custom tag-helpers?

I noticed in my project that all my form fields follow the same pattern. A typical example is:
<div class="col-x-x">
<label asp-for="Property"></label>
<span message="description">
<input asp-for="Property" />
<span asp-validation-for="Property"></span>
</div>
I would love to have some way of grouping this code so that i simply pass it the property on the model and it outputs the correct HTML. e.g.:
<form-field for="Property" ...>
or
#Html.StringFormField(...)
The issue I am having is that whatever method I try, the html outputted is the original html above, and not the html that is generated from the tag helpers. I have tried both methods and neither have been successful. Additionally I have tried to create a razor function, but all my attempts fail to compile, and I can't make a partial view work as I haven't been able to find a way to get the property information after passing a string to a view.
My latest attempt was using a tag helper, however this had the same issue mentioned previously. The latest version of the code is as follows:
[HtmlTargetElement("form-field", Attributes = "for")]
public class FormFieldTagHelper : TagHelper
{
[HtmlAttributeName("for")]
public ModelExpression For { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "div";
output.TagMode = TagMode.StartTagAndEndTag;
var contentBuilder = new HtmlContentBuilder();
contentBuilder.AppendHtmlLine($"<label asp-for=\"{For}\"></label>");
contentBuilder.AppendHtmlLine($"<span message=\"description.\"></span>");
contentBuilder.AppendHtmlLine($"<input asp-for=\"{For}\"/>");
contentBuilder.AppendHtmlLine($"<span asp-validation-for=\"{For}\"/></span>");
output.Content.SetHtmlContent(contentBuilder);
}
}
There is an issue addressing this (with no solution) which suggested the order of the imports was a potential issue, so my imports are as follows:
#addTagHelper Project.Web.Features.Shared.*, Project.Web
#addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
Any solution would be welcome, either for a tag helper or another method.
You could use IHtmlGenerator to generate these elements, refer to my below demo code:
[HtmlTargetElement("form-field", Attributes = "for")]
public class FormFieldTagHelper : TagHelper
{
[HtmlAttributeName("for")]
public ModelExpression For { get; set; }
private readonly IHtmlGenerator _generator;
[ViewContext]
public ViewContext ViewContext { get; set; }
public FormFieldTagHelper(IHtmlGenerator generator)
{
_generator = generator;
}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
using (var writer = new StringWriter())
{
writer.Write(#"<div class=""form-group"">");
var label = _generator.GenerateLabel(
ViewContext,
For.ModelExplorer,
For.Name, null,
new { #class = "control-label" });
label.WriteTo(writer, NullHtmlEncoder.Default);
writer.Write(#"<span message=""description.""></span>");
var textArea = _generator.GenerateTextBox(ViewContext,
For.ModelExplorer,
For.Name,
For.Model,
null,
new { #class = "form-control" });
textArea.WriteTo(writer, NullHtmlEncoder.Default);
var validationMsg = _generator.GenerateValidationMessage(
ViewContext,
For.ModelExplorer,
For.Name,
null,
ViewContext.ValidationMessageElement,
new { #class = "text-danger" });
validationMsg.WriteTo(writer, NullHtmlEncoder.Default);
writer.Write(#"</div>");
output.Content.SetHtmlContent(writer.ToString());
}
}
}
View:
<form-field for="ManagerName"></form-field>
Result:
It seems the easiest way to do this without duplicating custom tag helper code with the html generator is by simply creating new instances of the custom tag helpers from within a new tag helper.
e.g.
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "div";
output.TagMode = TagMode.StartTagAndEndTag;
//create label tag
LabelForTagHelper labelTagHelper = new LabelForTagHelper(ValidatorFactory)
{
For = this.For,
IgnoreRequired = this.IgnoreRequired
};
TagHelperOutput labelOutput = new TagHelperOutput(
tagName: tagName,
attributes: attributes ?? new TagHelperAttributeList(),
getChildContentAsync: (s, t) =>
{
return Task.Factory.StartNew<TagHelperContent>(() => new DefaultTagHelperContent());
}
);
var labelElement = await labelTagHelper.ProcessAsync(context, labelOutput);
output.Content.AppendHtml(labelElement );
//repeat for other tags
}