实现ASP.NET MVC3 HtmlHelper 的 RadioButtonList 与CheckBoxList 扩展
ASP.NET MVC3也出来有一段时间了,对于没有RadioButtonList 与CheckBoxList的问题,网上也已经有很多解决方案了,可以for循环拼接出来,也可以引用ASP.NET MVC Toolkit,等等方法。其实本没有必要写出来的,不过看了WebGird中队format的实现方式,一时来了兴趣,就尝试这实现了一下,发现还是有不少机关的,于是就拿出来和大家分享一下。
首先清楚下要实现什么,For<TModel, TProperty> 这样的重载时必须的了,然后还要实现WebGrid的format类似的支持,可以自定义输出格式。当然输出正确的列表HTML是必须的,不过这个不难。
那么就先实现一个最基本的核心函数
using System;
using System.Text;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Web;
using System.Web.Mvc;
namespace SymApplauseAward.MvcExtention
{public static class CheckBoxListExtension{public static MvcHtmlString InputListInternal(this HtmlHelper html,
string name,
IEnumerable<SelectListItem> selectList,bool allowMultiple,
){string fullHtmlFieldName =
html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);if (string.IsNullOrEmpty(fullHtmlFieldName)){throw new ArgumentException("filed can't be null or empty !","name");
}StringBuilder strBuilder = new StringBuilder();
TagBuilder tagBuilder = new TagBuilder("input");foreach (var item in selectList){ //Clear first
tagBuilder.InnerHtml = string.Empty;
if (allowMultiple)
{tagBuilder.MergeAttribute("type", "checkbox", true);}else
{tagBuilder.MergeAttribute("type", "radio", true);}tagBuilder.MergeAttribute("value", item.Value, true);if (item.Selected)
tagBuilder.MergeAttribute("selected", "selected", true);tagBuilder.MergeAttribute("name", fullHtmlFieldName, true);var s = tagBuilder.ToString()+item.Text+"<br/>"
strBuilder.Append(s);}return new MvcHtmlString(strBuilder.ToString());}}
这里通过TagBuilder创建Html,虽然也可以直接拼字符串,不过既然微软提供了相应的类,就直接用吧。 并且通过html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);获得了合法的name值,这步很重要,它会把“.”转化为“_”从而使name合法。
好了,基本功能实现了,如果用@Html.InputListInternal去调用,就可以显示相应的List了。
不过,这样还不太灵活,我们无法自定义格式,那么下面就实现自定义格式的功能。查看一下WebGrid的文档,原来format参数的类型是Func<dynamic,object>类型,那简单我们加一个好了。加完后代码如下:
public static MvcHtmlString InputListInternal(this HtmlHelper html,
string name,
IEnumerable<SelectListItem> selectList,bool allowMultiple,
Func<dynamic, object> format
){//...
if (format == null)format = i => i.Button + i.Text + "<br/>";
StringBuilder strBuilder = new StringBuilder();
TagBuilder tagBuilder = new TagBuilder("input");foreach (var item in selectList){//...
var inputItem = new { Button = tagBuilder.ToString(),Text = item.Text };
var s = format(inputItem).ToString();strBuilder.Append(s);}return new MvcHtmlString(strBuilder.ToString());}
赶快用一下@Html.InputListInternal(“Score”,scores,false,item=>item.Button+”>”+item.Text),事与愿违,报错了“object 不包含Button的定义”。奇怪了,明明传入的是new {Button=tagBuilder.ToString(), Text=item.Text},有Button属性啊!
于是开始查技术文档,原来是dynamic的循环不会涉及扩展方法,换句话说,声明的匿名对象对于dynamic来说是不知道的,于是就强制转换为Object了,Object当然没有Button属性,于是报错了。对于这种问题,有两种解决办法,方法一:不用扩展方法的调用方式,而是回归静态类静态方法调用的方式。方法二:在dynamic的上下文中引用类型。第一种方法不说了,要那样就不做这个扩展了,于是尝试第二种解决办法。
由于是在页面定义的Lamda表达式,因此Context是Page。想想的话,要用这个扩展必然要引用这个命名空间,那么只要传回的类型是这个空间的public类,那么dynamic就能找到相应的类型了。于是定义了一个新类InputListItem,将代码稍作改动:
var btnHtmlString = new MvcHtmlString(tagBuilder.ToString());
var inputItem = new InputListItem { Button = btnHtmlString, Text = item.Text };
var s = format(inputItem).ToString();strBuilder.Append(s);
再试下,OK,显示正常了,而且也是自定义的格式,至此format事情是搞定了。
最后实现一下For<TModel, TProperty>的重载,这个比较简单,就是传入一个Expression<Func<TModel, TProperty>>的参数,这里多提示一下可以用ExpressionHelper.GetExpressionText来直接提取参数的内容。至于匿名类型的htmlAttributes的处理也可以用HtmlHelper.AnonymousObjectToHtmlAttributes简单完成。在经过进一步封装后,最终代码如下:
using System;
using System.Text;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Web;
using System.Web.Mvc;
namespace Pride_Zhou.MvcExtention
{public class InputListItem{public MvcHtmlString Button { get; set; }public string Text { get; set; }}public static class CheckBoxListExtension{#region CheckBoxListpublic static MvcHtmlString CheckBoxListFor<TModel, TProperty>(this HtmlHelper<TModel> html,
Expression<Func<TModel, TProperty>> expression,IEnumerable<SelectListItem> selectList,Func<dynamic, object> format,
object htmlAttributes)
{return CheckBoxListFor(html, expression, selectList, format, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
}public static MvcHtmlString CheckBoxListFor<TModel, TProperty>(this HtmlHelper<TModel> html,
Expression<Func<TModel, TProperty>> expression,IEnumerable<SelectListItem> selectList,Func<dynamic, object> format = null,IDictionary<string, object> htmlAttributes = null){return CheckBoxList(html, GetName(expression), selectList, format, htmlAttributes);
}public static MvcHtmlString CheckBoxList(this HtmlHelper html,
string name,
IEnumerable<SelectListItem> selectList,Func<dynamic, object> format,
object htmlAttributes)
{return CheckBoxList(html, name, selectList, format, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
}public static MvcHtmlString CheckBoxList(this HtmlHelper html,
string name,
IEnumerable<SelectListItem> selectList,Func<dynamic, object> format = null,IDictionary<string, object> htmlAttributes = null){return InputListInternal(html, name, selectList, true, format, htmlAttributes);}#endregion#region RadioButtonListpublic static MvcHtmlString RadioButtonList(this HtmlHelper html,
string name,
IEnumerable<SelectListItem> selectList,Func<dynamic, object> format,
object htmlAttributes)
{return RadioButtonList(html, name, selectList, format, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
}public static MvcHtmlString RadioButtonList(this HtmlHelper html,
string name,
IEnumerable<SelectListItem> selectList,Func<dynamic, object> format = null,IDictionary<string, object> htmlAttributes = null){return InputListInternal(html, name, selectList, false, format, htmlAttributes);}public static MvcHtmlString RadioButtonListFor<TModel, TProperty>(this HtmlHelper<TModel> html,
Expression<Func<TModel, TProperty>> expression,IEnumerable<SelectListItem> selectList,Func<dynamic, object> format,
object htmlAttributes)
{return RadioButtonList(html, GetName(expression), selectList, format, htmlAttributes);
}public static MvcHtmlString RadioButtonListFor<TModel, TProperty>(this HtmlHelper<TModel> html,
Expression<Func<TModel, TProperty>> expression,IEnumerable<SelectListItem> selectList,Func<dynamic, object> format = null,IDictionary<string, object> htmlAttributes = null){return RadioButtonList(html, GetName(expression), selectList, format, htmlAttributes);
}#endregion/*-------------------------------------
* Core Function--------------------------------------*/private static MvcHtmlString InputListInternal(this HtmlHelper html,
string name,
IEnumerable<SelectListItem> selectList,bool allowMultiple,
Func<dynamic, object> format,
IDictionary<string, object> htmlAttributes){string fullHtmlFieldName = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);
if (string.IsNullOrEmpty(fullHtmlFieldName)){throw new ArgumentException("filed can't be null or empty !", "name");}if (format == null)format = i => i.Button + i.Text + "<br/>";
StringBuilder strBuilder = new StringBuilder();
TagBuilder tagBuilder = new TagBuilder("input");foreach (var item in selectList){ //Clear first
tagBuilder.InnerHtml = string.Empty;
if (allowMultiple)
{tagBuilder.MergeAttribute("type", "checkbox", true);}else
{tagBuilder.MergeAttribute("type", "radio", true);}tagBuilder.MergeAttribute("value", item.Value, true);if (item.Selected)
tagBuilder.MergeAttribute("selected", "selected", true);tagBuilder.MergeAttributes<string, object>(htmlAttributes);tagBuilder.MergeAttribute("name", fullHtmlFieldName, true);var btnHtmlString = new MvcHtmlString(tagBuilder.ToString());
var inputItem = new InputListItem { Button = btnHtmlString, Text = item.Text };
var s = format(inputItem).ToString();strBuilder.Append(s);}return new MvcHtmlString(strBuilder.ToString());}private static string GetName(LambdaExpression expression){if (expression == null){throw new ArgumentNullException("expression");}return ExpressionHelper.GetExpressionText(expression);
}}}
好了,现在可以像WebGrid一样生成批量Html了,例如:
<table style="border:0 solid #000;"><tbody>@Html.RadioButtonList(String.Format("{0}{1}", "S_", gId),Model.GetQuestions(gId).ToList().Select(q => new SelectListItem { Value = q.Id.ToString(), Text = q.Content }),@<tr style="border:0 solid #000;"><td style="width:2em;border:0 solid #000;">@item.Button</td><td style="border:0 solid #000;">@item.Text</td></tr>, new { @class = "score"})</tbody></table>
怎样,和WebGird一样吧,如果觉得item参数过少,扩展InputListItem类就可以了。 总体来说实现不难,就是Dynamic比较坑爹,这里很多代码是通过查看MVC源码的实现来写的,当然检查还不够完美,如果有兴趣可以仿照微软,加上相应的代码,这样用起来也更稳定。