Basically, I'm extending on a previously answered question (Updating related entities) so that it is a Custom Tag Helper.
I want to send the custom tag helper a list of phones related to the user and generate a textbox for each.
So, lets assume I have the following syntax:
<user-phones phones="@Model.UserPhones" />
Here is the start I have for the Custom Tag Helper:
public class UserPhonesTagHelper : TagHelper
{
private readonly IHtmlGenerator _htmlGenerator;
private const string ForAttributeName = "asp-for";
public List<UserPhones> Phones { get; set; }
[ViewContext]
public ViewContext ViewContext { set; get; }
[HtmlAttributeName(ForAttributeName)]
public ModelExpression For { get; set; }
public UserPhonesTagHelper(IHtmlGenerator htmlGenerator)
{
_htmlGenerator = htmlGenerator;
}
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "div";
output.TagMode = TagMode.StartTagAndEndTag;
//output.Attributes.Add("class", "form-group");
StringBuilder sbRtn = new StringBuilder();
for (int i = 0; i < Phones.Count(); i++)
{
//NEED HELP HERE
}
output.Content.SetHtmlContent(sbRtn.ToString());
}
}
Within the for
loop, how could I generate a textbox and hidden inputs related to the current `UserPhone' entity in the iteration? I would need this to remain bound when the parent razor page is posted as well.
My thought is a method like so would help. BUT, I do not know how to pass the ModelExpression
from the for
loop to the method
private void WriteInput(TextWriter writer)
{
var tagBuilder = _htmlGenerator.GenerateTextBox(
ViewContext,
For.ModelExplorer,
For.Name,
value: null,
format: null,
htmlAttributes: new { @class = "form-control" });
tagBuilder.WriteTo(writer, htmlEncoder);
}
Thank you again for all your help... still learning asp.net core.
You're almost there.
Design
The difficulty here is that we need to construct an expression for unknown properties. Let's say when you want to use the <user-phones asp-for=""/>
in a much more higher level, considering the following code :
@model M0
@{
var M1 = GetM1ByMagic(M0);
}
<user-phones asp-for="@M1.M2....Mx.UserPhones">
</user-phones>
Inside the tag helper, we might assume the default name of each property to be UserPhones[<index>].<property-name>
. But that's not always that case, users might want to change it to M0.M2....Mx.UserPhones[<index>].<property-name>
. However, it's not possible to know how many levels there will be at compile-time.
So we need an attribute of ExpressionFilter
to convert the default expression to target expression :
public class UserPhonesTagHelper : TagHelper
{
[HtmlAttributeName("expression-filter")]
public Func<string, string> ExpressionFilter { get; set; } = e => e;
// ...
}
The ExpressionFilter
here is a simple delegate to convert expression string.
Show me the Code
I simply copy most of your code and make a little change :
public class UserPhonesTagHelper : TagHelper
{
private readonly IHtmlGenerator _htmlGenerator;
private const string ForAttributeName = "asp-for";
public IList<UserPhones> Phones { get; set; }
[ViewContext]
public ViewContext ViewContext { set; get; }
[HtmlAttributeName(ForAttributeName)]
public ModelExpression For { get; set; }
public UserPhonesTagHelper(IHtmlGenerator htmlGenerator)
{
_htmlGenerator = htmlGenerator;
}
[HtmlAttributeName("expression-filter")]
public Func<string, string> ExpressionFilter { get; set; } = e => e;
// a helper method that generate a label and input for some property
private TagBuilder GenerateSimpleInputForField( int index ,PropertyInfo pi)
{
var instance = Phones[index];// current instance of a single UserPhone
var name = pi.Name; // property name : e.g. "PhoneNumberId"
var v = pi.GetValue(instance);
var div = new TagBuilder("div");
div.AddCssClass("form-group");
var expression = this.ExpressionFilter(For.Name + $"[{index}].{name}");
var explorer = For.ModelExplorer.GetExplorerForExpression(typeof(IList<UserPhones>), o =>v);
var label = _htmlGenerator.GenerateLabel( ViewContext, explorer, expression, name, new { } );
div.InnerHtml.AppendHtml(label);
var input = _htmlGenerator.GenerateTextBox( ViewContext, explorer, expression, v, null, new { @class = "form-control" } );
div.InnerHtml.AppendHtml(input);
return div;
}
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "div";
output.TagMode = TagMode.StartTagAndEndTag;
var type = typeof(UserPhones);
PropertyInfo phoneId= type.GetProperty("UserPhoneId");
PropertyInfo phoneNumber= type.GetProperty("PhoneNumber");
for (int i = 0; i< Phones.Count();i++) {
var div1 = this.GenerateSimpleInputForField(i,phoneId);
var div2 = this.GenerateSimpleInputForField(i,phoneNumber);
output.Content.AppendHtml(div1);
output.Content.AppendHtml(div2);
}
}
}
The ProcessAsync()
above only shows a label and input for UserPhoneId
and PhoneNumber
field. If you would like to show all the properties automatically, you can simply change the method to be :
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "div";
output.TagMode = TagMode.StartTagAndEndTag;
for (int i = 0; i < Phones.Count(); i++)
{
var pis = typeof(UserPhones).GetProperties();
foreach (var pi in pis)
{
var div = this.GenerateSimpleInputForField(i, pi);
output.Content.AppendHtml(div);
}
}
}
the default expression string for some field is generated by:
get_the_name_by('asp-for') +'[<index>]'+'<property-name>'
eg :AppUser.UserPhones[i].<property-name>
Surely it won't apply for all cases, we can custom our own expression-filter
to convert the expression as we like :
// use <user-phones> in view file :
// custom our own expression filter :
@{
var regex= new System.Text.RegularExpressions.Regex(@"...");
Func<string, string> expressionFilter = e => {
var m = regex.Match(e);
// ...
return m.Groups["expression"].Value;
};
}
<user-phones phones="@Model.AppUser.UserPhones"
asp-for="@Model.AppUser.UserPhones"
expression-filter="expressionFilter">
</user-phones>
Test case
<div class="row">
@await Html.PartialAsync("_NameAndID", Model.AppUser)
</div>
<form method="post">
<div class="row">
<user-phones phones="@Model.AppUser.UserPhones" asp-for="@Model.AppUser.UserPhones" expression-filter="e => e.Substring(8)"></user-phones>
</div>
<button type="submit">submit</button>
</form>
The first part is generated by partial view and the second part is generated by user-phones
: