自定义 CheckBoxListFor Html 助手。如何绑定到视图模型 属性?

Custom CheckBoxListFor Html helper. How do I bind to a viewmodel property?

我正在创建一个自定义 Html 帮助程序来处理复选框列表。我知道网上有大量关于这个主题的信息,但我还没有看到太多关于尝试以下面的方式生成 HTML 的信息。

需要渲染HTML

<ul>
    <li>
         <label>
             <input type="checkbox" name="Genres" value="SF" />
             Science Fiction
         </label>
    </li>
    <li>
         <label>
             <input type="checkbox" name="Genres" value="HR" />
             Horror
         </label>
    </li>
    <!-- more genres -->
</ul>

我使用不同的复选框 而不仅仅是布尔值,因为我想利用这样一个事实,即在表单 post 上,我会得到一个逗号- 选定 值的分隔列表。例如,如果同时选择科幻和恐怖,我会得到 "SF,HR".

查看模型

public class MovieViewModel
{
    public string Name { get;set; }
    public string Year { get;set; }
    public IEnumerable<string> Genres { get;set; }
    public IEnumerable<SelectListItem> GenreOptions { get;set; }
}

查看

在我看来,我想基本上这样做:

@Html.EditorFor(model => model.Name)
@Html.EditorFor(model => model.Year)
@Html.CheckBoxListFor(model => model.Genres, Model.GenreOptions)

自定义 Html 助手

所以,我开始创建一个自定义的 Html 助手,但我在这些方面没有太多经验,这就是我需要帮助的地方:

    public static MvcHtmlString CheckboxListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
        Expression<Func<TModel, TProperty>> expression,
        IEnumerable<SelectListItem> items)
    {
        if (items == null) return null;

        var itemsList = items.ToList();
        if (!itemsList.Any()) return null;

       // How do I get "Genres" name from the passed expression from viewmodel, without passing the hardcoded string "genres"?
        var checkboxGroupName = expression.what????

        var sb = new StringBuilder();
        sb.Append("<ul>");

        foreach (var item in itemsList)
        {
            var checkbox = $"<input type=\"checkbox\" name=\"{checkboxGroupName}\" value=\"{item.Value}\" />";
            sb.Append($"<li class=\"checkbox\"><label>{checkbox} {HttpUtility.HtmlEncode(item.Text)}</label></li>");
        }

        sb.Append("</ul>");

        return MvcHtmlString.Create(sb.ToString());
    }

正如您在上面的评论中看到的,我不知道如何将视图模型 属性 名称 "Genres" 动态分配给复选框的 name 属性,而不对其进行硬编码.我如何从 model => model.Genres 表达式中获取它?

您可以从使用 ModelMetaData class 使用以下代码行:

var metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
var checkboxGroupName = metadata.PropertyName;

这是一篇解释How to Create Custom Strongly Typed Html Helpers的文章,可能对您有更多帮助。

要获取 属性 名称,请使用

var name = ExpressionHelper.GetExpressionText(expression);

这将给出完全限定的名称,例如 @CheckboxListFor(m => m.Movies.Genres, ....) 将 return Movies.Genres.

但是要考虑到您为模型使用自定义 EditorTemplate 的情况(这是父模型的 属性),那么您还需要获取 `HtmlFieldPrefix 并且如果它存在,在其前面加上名称。

您当前的代码没有为您提供正确的绑定,例如,如果 Genres 包含与 SelectListItemValue 属性 相匹配的值,则关联的复选框应该是渲染视图时检查。你的代码应该是

public static MvcHtmlString CheckBoxListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> items)
{
    if (items == null)
    {
        throw new ArgumentException("...");
    }
    var metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
    var model = metadata.Model as IEnumerable<string>;
    if (model == null)
    {
        throw new ArgumentException("...");
    }
    // Get the property name
    var name = ExpressionHelper.GetExpressionText(expression);
    // Get the prefix in case using a EditorTemplate for a nested property
    string prefix = htmlHelper.ViewData.TemplateInfo.HtmlFieldPrefix;
    if (!String.IsNullOrEmpty(prefix))
    {
        name = String.Format("{0}.{1}", prefix, name);
    }
    StringBuilder html = new StringBuilder();
    foreach (var item in items)
    {
        StringBuilder innerHtml = new StringBuilder();
        TagBuilder checkbox = new TagBuilder("input");
        checkbox.MergeAttribute("type", "checkbox");
        checkbox.MergeAttribute("name", name);
        checkbox.MergeAttribute("value", item.Value);
        if (model.Contains(item.Value))
        {
            checkbox.MergeAttribute("checked", "checked");
        }
        innerHtml.Append(checkbox.ToString());
        TagBuilder text = new TagBuilder("span");
        text.InnerHtml = item.Text;
        innerHtml.Append(text.ToString());
        TagBuilder label = new TagBuilder("label");
        label.InnerHtml = innerHtml.ToString();
        TagBuilder li = new TagBuilder("li");
        li.AddCssClass("checkbox");
        li.InnerHtml = label.ToString();
        html.Append(li);
    }
    TagBuilder ul = new TagBuilder("ul");
    ul.InnerHtml = html.ToString();
    return MvcHtmlString.Create(ul.ToString());
}