使用自定义标签助手更新相关 Phone 个实体

Updating related Phone entities with custom tag helper

根据我目前的申请,每个 AppUser 可能(或可能不)有 3 个 phone 号码 (UserPhones)。每种类型(移动、家庭、其他)一种。

以下 Tag Helper 效果很好(感谢@itminus)。

从 Razor 页面调用代码:

<user-phones phones="@Model.UserPhones" 
              asp-for="@Model.UserPhones" 
              prop-name-to-edit="PhoneNumber"
              types-to-edit="new EnumPhoneType[] { EnumPhoneType.Mobile, 
                               EnumPhoneType.Other }" />

代码:

public class UserPhonesTagHelper : TagHelper
{
    private readonly IHtmlGenerator _htmlGenerator;
    private const string ForAttributeName = "asp-for";

    [HtmlAttributeName("expression-filter")]
    public Func<string, string> ExpressionFilter { get; set; } = e => e;


    public List<UserPhones> Phones { get; set; }
    public EnumPhoneType[] TypesToEdit { get; set; }
    public string PropNameToEdit { 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 = null; //DO NOT WANT AN OUTTER HTML ELEMENT

        for (int i = 0; i < Phones.Count(); i++)
        {
            var props = typeof(UserPhones).GetProperties();
            var pType = props.Single(z => z.Name == "Type");
            var pTypeVal = pType.GetValue(Phones[i]);
            EnumPhoneType eType = (EnumPhoneType) Enum.Parse(typeof(EnumPhoneType), pTypeVal.ToString());

            string lVal = null;
            switch (eType)
            {
                case EnumPhoneType.Home:
                    lVal = "Home Phone";
                    break;
                case EnumPhoneType.Mobile:
                    lVal = "Mobile Phone";
                    break;
                case EnumPhoneType.Other:
                    lVal = "Other Phone";
                    break;
                default:
                    break;
            }

            //LOOP ALL PROPERTIES
            foreach (var pi in props)
            {
                var v = pi.GetValue(Phones[i]);
                var expression = this.ExpressionFilter(For.Name + $"[{i}].{pi.Name}");
                var explorer = For.ModelExplorer.GetExplorerForExpression(typeof(IList<UserPhones>), o => v);

                //IF REQUESTED TYPE AND PROPERTY SPECIFIED
                if (pi.Name.NormalizeString() == PropNameToEdit.NormalizeString() && TypesToEdit.Contains(eType))
                {
                    TagBuilder gridItem = new TagBuilder("div");
                    gridItem.Attributes.Add("class", "rvt-grid__item");
                    gridItem.InnerHtml.AppendHtml(BuildLabel(explorer, expression, lVal));
                    gridItem.InnerHtml.AppendHtml(BuildTextBox(explorer, expression, v.ToString()));
                    output.Content.AppendHtml(gridItem);
                }
                else //ADD HIDDEN FIELD SO BOUND PROPERLY
                    output.Content.AppendHtml(BuildHidden(explorer, expression, v.ToString()));
            }
        }
    }

    private TagBuilder BuildTextBox(ModelExplorer explorer, string expression, string v)
    {
        return _htmlGenerator.GenerateTextBox(ViewContext, explorer, expression, v, null, new { @class = "form-control" });
    }

    public TagBuilder BuildHidden(ModelExplorer explorer, string expression, string v)
    {
        return _htmlGenerator.GenerateHidden(ViewContext, explorer, expression, v, false, new { });
    }

    public TagBuilder BuildLabel(ModelExplorer explorer, string expression, string v)
    {
        return _htmlGenerator.GenerateLabel(ViewContext, explorer, expression, v, new { });
    }
}

我的问题:

让我们假设这个 AppUser 目前只列出了一个相关的手机 phone 号码。所以 AppUser.UserPhones(移动类型的计数 = 1)。所以上面的代码,原样,将只为移动 phone.

呈现输入

由于 types-to-edit 同时调用移动和其他,我希望将两种输入都呈现到屏幕上。如果用户将 phone 数字添加到其他输入,则它将保存到 Razor Pages OnPostAsync 方法上的相关 UserPhones 实体。如果用户没有为 "Other" 输入提供数字,则不应创建 "Other" 类型的相关 UserPhones 记录。

你能帮忙吗?

再次感谢!!!

标签助手

As my application currently sits, each AppUser may (or may not) have 3 phone numbers (UserPhones). One of each type (Mobile, Home, Other).

如果我理解正确,一个 AppUser 可能有 3 个 phone 数字,每个用户的每个 phone 类型的计数将为零或一个。

如果是这样,我们可以简单地使用PhoneType作为索引,换句话说,不需要使用自定义索引来遍历Phones 属性和ProcessAsync() 方法可以是:

    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        output.TagName = null; //DO NOT WANT AN OUTTER HTML ELEMENT

        var props = typeof(UserPhones).GetProperties();

        // display editable tags for phones
        foreach (var pt in this.TypesToEdit) {
            var phone = Phones.SingleOrDefault(p=>p.Type == pt);
            var index = (int) pt;
            foreach (var pi in props)
            {
                // if phone==null , then the pv should be null too
                var pv = phone==null? null: pi.GetValue(phone);
                var tag = GenerateFieldForProperty(pi.Name, pv, index, pt);
                output.Content.AppendHtml(tag);
            }
        }
        // generate hidden input tags for phones
        var phones= Phones.Where(p => !this.TypesToEdit.Contains((p.Type)));
        foreach (var p in phones) {
            var index = (int)p.Type;
            foreach (var pi in props) {
                var pv = pi.GetValue(p);
                var tag = GenerateFieldForProperty(pi.Name,pv,index,p.Type);
                output.Content.AppendHtml(tag);
            }
        }
    }

这里的 GenerateFieldForProperty 是为特定 属性:

生成标签生成器的简单辅助方法
    private TagBuilder GenerateFieldForProperty(string propName,object propValue,int index, EnumPhoneType eType )
    {
        // whether current UserPhone is editable (check the PhoneType)
        var editable = TypesToEdit.Contains(eType);
        var expression = this.ExpressionFilter(For.Name + $"[{index}].{propName}");
        var explorer = For.ModelExplorer.GetExplorerForExpression(typeof(IList<UserPhones>), o => propValue);

        //IF REQUESTED TYPE AND PROPERTY SPECIFIED
        if (pi.Name.NormalizeString() == PropNameToEdit.NormalizeString() && editable)
        {
            TagBuilder gridItem = new TagBuilder("div");
            gridItem.Attributes.Add("class", "rvt-grid__item");
            var labelText = this.GetLabelTextByPhoneType(eType);
            gridItem.InnerHtml.AppendHtml(BuildLabel(explorer, expression, labelText));
            gridItem.InnerHtml.AppendHtml(BuildTextBox(explorer, expression, propValue?.ToString()));
            return gridItem;
        }
        else //ADD HIDDEN FIELD SO BOUND PROPERLY
            return BuildHidden(explorer, expression, propValue?.ToString());
    }


    private string GetLabelTextByPhoneType(EnumPhoneType eType) {
        string lVal = null;
        switch (eType)
        {
            case EnumPhoneType.Home:
                lVal = "Home Phone";
                break;
            case EnumPhoneType.Mobile:
                lVal = "Mobile Phone";
                break;
            case EnumPhoneType.Other:
                lVal = "Other Phone";
                break;
            default:
                break;
        }
        return lVal;
    }

当发布到服务器时,如果有人没有为 other PhoneType 输入 phone 号码,实际负载将类似于:

AppUser.UserPhones[0].UserPhoneId=....&AppUser.UserPhones[0].PhoneNumber=911&....
&AppUser.UserPhones[2].UserPhoneId=&AppUser.UserPhones[2].PhoneNumber=&AppUser.UserPhones[2].Type=&AppUser.UserPhones[2].AppUserId=&AppUser.UserPhones[2].AppUser=
&AppUser.UserPhones[1].UserPhoneId=...&AppUser.UserPhones[1].PhoneNumber=119&....

由于我们使用 phone 类型作为索引,我们可以得出结论,UserPhones[0] 将用作 Mobile phone 和 UserPhones[2]将被视为 Home phone.

页面处理程序或操作方法

并且服务器端的模型绑定器将为每个 UserPhone 创建一个空字符串。 为了删除那些空输入并防止过度发布攻击,我们可以使用 Linq 过滤 UserPhones 以便我们可以创建或更新没有空电话的 UserPhone 记录:

    var editables = new[] {
        EnumPhoneType.Mobile,
        EnumPhoneType.Other,
    };
    AppUser.UserPhones = AppUser.UserPhones
        .Where(p => !string.IsNullOrEmpty(p.PhoneNumber))  // remove empty inputs
        .Where(p => editables.Contains(p.Type) )           // remove not editable inputs
        .ToList();
    // now the `UserPhones` will be clean for later use
    // ... create or update user phones as you like 

假设您要创建 phones :

public IActionResult OnPostCreate() {
    var editables = new[] {
        EnumPhoneType.Mobile,
        EnumPhoneType.Other,
    };
    AppUser.UserPhones = AppUser.UserPhones
        .Where(p => !string.IsNullOrEmpty(p.PhoneNumber))
        .Where(p => editables.Contains(p.Type) )
        .Select(p => {                   // construct relationship for inputs
            p.AppUser = AppUser;
            p.AppUserId = AppUser.Id;
            return p;
        })
        .ToList();

    this._dbContext.Set<UserPhones>().AddRange(AppUser.UserPhones);
    this._dbContext.SaveChanges();

    return Page();
}

测试用例:

<form method="post">
    <div class="row">

    <user-phones 
        phones="@Model.AppUser.UserPhones" 
        asp-for="@Model.AppUser.UserPhones" 
        prop-name-to-edit="PhoneNumber"
        types-to-edit="new EnumPhoneType[] { EnumPhoneType.Mobile, EnumPhoneType.Other}"
        >
    </user-phones>
    </div>

    <button type="submit">submit</button>
</form>

拥有手机 phone 和家庭 phone 号码的用户 1:

想要创建新手机 phone 号码的用户 2: