问题
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.
回答1:
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 forUserPhoneId
andPhoneNumber
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
:
来源:https://stackoverflow.com/questions/53395905/complex-custom-tag-helper