How to generate a Razor Page url within a custom TagHelper - asp.net-core

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.

Related

Get full HTML field name for client side validation in 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 );

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:

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
}

Access attribute from outside of a controller

Is it possible to access a custom attribute of a controller action from outside of that controller? I have a custom output formatter that should return a file with a specific name. I made a custom attribute that accepts a string (filename) and I'd like to try to access the value of that attribute from within the custom output formatter.
public class FileAttribute : Attribute
{
public ExcelTemplateAttribute(string fileName)
{
FileName = fileName;
}
public string FileName { get; }
}
My OutputFormatter looks like this:
public class FileOutputFormatter : OutputFormatter
{
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
{
// string filename = ???
}
}
My API action returns a service
[File("Template.txt")]
public IActionResult Get([FromQuery]int Id)
{
IEnumerable<int> data = _kenoReport.GetReportData(Id);
return Ok(data);
}
Controller and/or action information is not easily accessible outside of the MVC-specific parts of the middleware pipeline without resorting to complex (and easy-to-break) code relying on reflection.
However, one workaround is to use an action filter to add the attribute details to the HttpContext.Items dictionary (which is accessible throughout the entire middleware pipeline) and have the output formatter retrieve it later on in the middleware pipeline.
For example, you could make your FileAttribute derive from ActionFilterAttribute and have it add itself to HttpContext.Items (using a unique object reference as the key) when executing:
public sealed class FileAttribute : ActionFilterAttribute
{
public FileAttribute(string filename)
{
Filename = filename;
}
public static object HttpContextItemKey { get; } = new object();
public string Filename { get; }
public override void OnActionExecuting(ActionExecutingContext context)
{
context.HttpContext.Items[HttpContextItemKey] = this;
}
}
Then in your output formatter you can retrieve the attribute instance and access the filename:
public sealed class FileOutputFormatter : OutputFormatter
{
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
{
if (context.HttpContext.Items.TryGetValue(FileAttribute.HttpContextItemKey, out var item)
&& item is FileAttribute attribute)
{
var filename = attribute.Filename;
// ...
}
}
}

How to process tag helper in asp.net core?

I want to know in ASP.NET Core 2.2 if there is a way to invoke TagHelper through code? I have custom TagHelper
public class EmailTagHelper : TagHelper
{
public string MailTo { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "a";
output.Attributes.SetAttribute("href", "mailto:" + MailTo);
output.Content.SetContent(MailTo);
}
}
Then in some render method in another class i want to use TagHelper to get corresponding markup
public override void Render(string email)
{
var emailTagHelper = new EmailTagHelper();
emailTagHelper.MailTo = email;
// How do i pass TagHelperContext and TagHelperOutput
emailTahHelper.Process(........);
//How do i get html string here
}
How do i process TagHelper though code here? Where would i get TagHelperContext and TagHelperOutput parameters and what method i need to invoke to get final html string?
I solved my issue by using TagBuilder instead of TagHelper.