I'm creating a custom Html helper for handling a checkbox list. I know there is a ton of info on this topic online, but I haven't seen much on trying to generate the HTML the way I am below.
<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>
I am using distinct checkbox values rather than just booleans, because I want to take advantage of the fact that on form post, I would get a comma-separated list of selected values. For example, if Sci-Fi and Horror were both selected, I'd get "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; }
}
In my view, I'd like to basically do this:
@Html.EditorFor(model => model.Name)
@Html.EditorFor(model => model.Year)
@Html.CheckBoxListFor(model => model.Genres, Model.GenreOptions)
So, I started creating a custom Html helper, but I don't have a ton of experience with these, and this is where I need the help:
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());
}
As you can see in the comment above, I don't know how to dynamically assign the viewmodel property name "Genres" to the checkboxes' name
attribute, without hardcoding it. How do I get it from model => model.Genres
expression?
To get the property name, use
var name = ExpressionHelper.GetExpressionText(expression);
which will give the fully qualified name, for example @CheckboxListFor(m => m.Movies.Genres, ....)
will return Movies.Genres
.
But to account for cases where your using an custom EditorTemplate
for you model (which is a property of a parent model), then you also need to get the `HtmlFieldPrefix and if it exists, prepend it the name.
You current code does not give you correct binding, for example if Genres
contains values matching the Value
property of a SelectListItem
, the associated checkbox should be checked when the view is rendered. Your code should be
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());
}