模型模板
前面的Html辅助器,如Html.CheckBoxFor和Html.TextBoxFor等,是明确指定了要使用的html元素。mvc框架支持另一种方法,叫做模板视图辅助器(Templated View Helper),在这样的辅助器中,指定想要显示或编辑的模型对象或属性,而让mvc框架去判断应该用什么样的html元素。
一、使用模板视图辅助器
1、为指定的模型属性生成html
使用模板视图辅助器,意味着我们不必考虑要指定用什么样的html元素来表现一个模型属性,而是只要说出想显示哪个属性,让mvc框架自己去判断采用什么html元素来表现它。
新建mvc3项目MvcApp,在解决方案管理器中鼠标右击文件夹“Models”,选择Add->Class,添加一个类库文件,取名为TestModelClass.cs,在里面建立如下类:
namespace MvcApp.Models { public class Person { public int PersonId { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public DateTime BirthDate { get; set; } public Address HomeAddress { get; set; } public bool IsApproved { get; set; } public Role Role { get; set; } } public class Address { public string Line1 { get; set; } public string Line2 { get; set; } public string City { get; set; } public string PostalCode { get; set; } public string Country { get; set; } } public enum Role { Admin, User, Guest } }
再新建HomeController控制器,完成Index动作方法:
public ActionResult Index() { Person p1 = new Person { FirstName = "Joe", LastName = "Smith", IsApproved = true }; return View(p1); }
在Index动作方法上添加默认试图Index.cshtml:
@model MvcApp.Models.Person @{ ViewBag.Title = "Index"; } <h2>Person</h2> <div class="field"> <label>Name:</label> @Html.EditorFor(x=>x.FirstName) @Html.EditorFor(x=>x.LastName) </div> <div class="field"> <label>Approved:</label> @Html.EditorFor(x=>x.IsApproved) </div>
这里使用了Html.EditorFor辅助器,它生成一个对属性进行编辑的html元素。显示结果为:
生成的html代码为:
<!DOCTYPE html> <html> <head> <title>Index</title> <link href="/Content/Site.css" rel="stylesheet" type="text/css" /> <script src="/Scripts/jquery-1.5.1.min.js" type="text/javascript"></script> </head> <body> <h2>Person</h2> <div class="field"> <label>Name:</label> <input class="text-box single-line" id="FirstName" name="FirstName" type="text" value="Joe" /> <input class="text-box single-line" id="LastName" name="LastName" type="text" value="Smith" /> </div> <div class="field"> <label>Approved:</label> <input checked="checked" class="check-box" id="IsApproved" name="IsApproved" type="checkbox" value="true" /><input name="IsApproved" type="hidden" value="false" /> </div> </body> </html>
可以看到,通过“x=>x.属性名”指定的属性名被用于id和name标签属性,并为FirstName和LastName属性创建了文本框(text),并为IsApproved属性创建了一个复选框(checkbox)。
也可以用另一种形式来生成html元素:以只读形式显示模型对象的值,不允许编辑。例如,将Index.cshtml修改为:
@model MvcApp.Models.Person @{ ViewBag.Title = "Index2"; } <h2>Person</h2> <div class="field"> <label>Name:</label> @Html.DisplayFor(x=>x.FirstName) @Html.DisplayFor(x => x.LastName) </div> <div class="field"> <label>Approved:</label> @Html.DisplayFor(x => x.IsApproved) </div>
执行后,显示结果为:
生成的html代码为:
<!DOCTYPE html> <html> <head> <title>Index2</title> <link href="/Content/Site.css" rel="stylesheet" type="text/css" /> <script src="/Scripts/jquery-1.5.1.min.js" type="text/javascript"></script> </head> <body> <h2>Person</h2> <div class="field"> <label>Name:</label> Joe Smith </div> <div class="field"> <label>Approved:</label> <input checked="checked" class="check-box" disabled="disabled" type="checkbox" /> </div> </body> </html>
现在的html可以让用户看到信息,但是不能编辑。由于大多数mvc程序都是要么显示数据,要么编辑数据,所以模板辅助器是很方便的。
Html.Display("FirstName") 以只读方式显示指定属性的值。
Html.DisplayFor(x=>x.FirstName) 上一辅助器的强类型版本。
Html.Editor("FirstName") 编辑指定属性的值,根据该属性的类型和元数据选择合适的编辑器,如文本框、复选框等。
Html.EditorFor(x=>x.FirstName) 上一辅助器的强类型版本。
Html.Label("FirstName") 用<label>标签显示指定属性的属性名,而不是属性值。
Html.LabelFor(x=>x.FirstName) 上一辅助器的强类型版本。
Html.DisplayText("FirstName") 绕过所有模板,渲染指定模型属性的简单字符串表示。
Html.DisplayTextFor(x=>x.FirstName) 上一辅助器的强类型版本。
2、为一个模型对象的所有属性生成html
除了可以单独为指定的模型属性生成html以外,还可以为一个模型对象的所有属性生成html的辅助器。这一个过程称为支架(scaffolding)。
Html.DisplayForModel()
Html.EditorForModel()
Html.LabelForModel()
都是针对整个模型对象里的所有属性来进行渲染。
例如,在HomeController中新建一个动作方法:
public ViewResult Scaffold() { Person p1 = new Person { PersonId = 1, FirstName = "Joe", LastName = "Smith", BirthDate = DateTime.Parse("2014/6/12"), IsApproved = true, Role = Role.User }; return View(p1); }
为它新建默认视图Scaffold.cshtml:
@model MvcApp.Models.Person @{ ViewBag.Title = "Scaffold"; } <h2>Person</h2> @Html.EditorForModel()
执行后的效果为:
在使用辅助器时,Html.EditorForModel()为模型对象中的属性生成html标签和编辑元素,不需要把模型对象作为参数传递给这个辅助器,因为是强类型。自动将每个元素都进行处理。
3、设置生成html的样式
查看该页面的html源代码:
<h2>Person</h2> <div class="editor-label"><label for="PersonId">PersonId</label></div> <div class="editor-field"><input class="text-box single-line" id="PersonId" name="PersonId" type="text" value="1" /> </div> <div class="editor-label"><label for="FirstName">FirstName</label></div> <div class="editor-field"><input class="text-box single-line" id="FirstName" name="FirstName" type="text" value="Joe" /> </div> <div class="editor-label"><label for="LastName">LastName</label></div> <div class="editor-field"><input class="text-box single-line" id="LastName" name="LastName" type="text" value="Smith" /> </div> <div class="editor-label"><label for="BirthDate">BirthDate</label></div> <div class="editor-field"><input class="text-box single-line" id="BirthDate" name="BirthDate" type="text" value="2014/6/12 0:00:00" /> </div> <div class="editor-label"><label for="IsApproved">IsApproved</label></div> <div class="editor-field"><input checked="checked" class="check-box" id="IsApproved" name="IsApproved" type="checkbox" value="true" /><input name="IsApproved" type="hidden" value="false" /> </div> <div class="editor-label"><label for="Role">Role</label></div> <div class="editor-field"><input class="text-box single-line" id="Role" name="Role" type="text" value="User" /> </div>
可以看到,每个元素显示的名字,和对应的编辑狂都是由<div>标签在控制,显示元素名字的<div>标签,用的class是"editor-label",构成编辑框的<div>用的class是"editor-field"。
打开解决方案管理器中的Content/Site.css文件,找到类.editor-label 和.editor-field,可以看到默认的样式为:
.editor-label { margin: 1em 0 0 0; } .editor-field { margin: 0.5em 0 0 0; }
现在想让同一个元素的名字和对应的编辑框在一行上,就可以修改这两个类的样式如下:
.editor-label { margin: 1em 0 0 0; clear:left; float:left; min-width: 100px; vertical-align:middle; } .editor-field { margin: 0.5em 0 0 0; width:150px; float:left; }
下面再执行,可以看到显示效果如下:
4、使用模型元数据
使用模板视图辅助器,尤其是使用它为一个模型对象的所有属性生成html时,有一个比较大的问题就是如何去控制这些属性,哪些需要显示,哪些不想显示出来,用什么类型显示等等。比如上一个例子,PersonId希望不要显示出来,因为正常情况下的程序几乎都不可能让用户直接去编辑Id值,第二个问题就是BirthDate显示成的是日期时间型,但是我们希望得到日期型。
这就需要采用模型元数据(Metadata)为这些辅助器提供指示,元数据是用注解属性来表示的,通过注解属性及参数值,给视图辅助器提供一系列指令。
(1)用元数据控制编辑及可见性
在Person类中,PersonId是不想让用户看到或编辑的属性。可以用HiddenInput注解属性,它会使辅助器渲染一个隐藏的input字段:
public class Person { [HiddenInput] public int PersonId { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public DateTime BirthDate { get; set; } public Address HomeAddress { get; set; } public bool IsApproved { get; set; } public Role Role { get; set; } }
使用[HiddenInput]这个注解属性时,Html.EditorFor和Html.EditorForModel辅助器会对这个被修饰的属性渲染一个只读字段,例如:
[HiddenInput]显示了PersonId属性的值,但用户不能编辑它。为该属性生成的html如下:
<div class="editor-label"><label for="PersonId">PersonId</label></div> <div class="editor-field">1<input id="PersonId" name="PersonId" type="hidden" value="1" /> </div>
id和name的值是PersonId,type的值是hidden,value的值是1,这是一个隐藏的input元素。当把这个编辑视图用于表单时,这个隐藏的input字段也是有用的。(模型绑定和模型验证还会用到)
如果想完全隐藏一个属性,可以把HiddenInput注解属性中的DisplayValue值设为false,如下:
public class Person { [HiddenInput(DisplayValue=false)] public int PersonId { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public DateTime BirthDate { get; set; } public Address HomeAddress { get; set; } public bool IsApproved { get; set; } public Role Role { get; set; } }
仍然是生成隐藏的input字段,以便PersonId属性的值可以被包含到任何要递交的表单中去。但使用DisplayValue=false,可以将属性整个隐藏起来,用户也看不到,而不只是不能编辑的问题。
查看html代码,可以看到如下的代码:
<h2>Person</h2> <input id="PersonId" name="PersonId" type="hidden" value="1" /> <div class="editor-label"><label for="FirstName">FirstName</label></div> <div class="editor-field"><input class="text-box single-line" id="FirstName" name="FirstName" type="text" value="Joe" /> </div> <div class="editor-label"><label for="LastName">LastName</label></div> <div class="editor-field"><input class="text-box single-line" id="LastName" name="LastName" type="text" value="Smith" /> </div> <div class="editor-label"><label for="BirthDate">BirthDate</label></div> <div class="editor-field"><input class="text-box single-line" id="BirthDate" name="BirthDate" type="text" value="2014/6/12 0:00:00" /> </div> <div class="editor-label"><label for="IsApproved">IsApproved</label></div> <div class="editor-field"><input checked="checked" class="check-box" id="IsApproved" name="IsApproved" type="checkbox" value="true" /><input name="IsApproved" type="hidden" value="false" /> </div> <div class="editor-label"><label for="Role">Role</label></div> <div class="editor-field"><input class="text-box single-line" id="Role" name="Role" type="text" value="User" /> </div>
另外,如果想把一个属性从生成的html中完全排除掉,而不仅仅是隐藏,那可以使用ScaffoldColumn注解属性。例如:
public class Person { [ScaffoldColumn(false)] public int PersonId { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public DateTime BirthDate { get; set; } public Address HomeAddress { get; set; } public bool IsApproved { get; set; } public Role Role { get; set; } }
当辅助器看到ScaffoldColumn注解属性时,会完全跳过该属性,不会创建隐藏input元素,该属性的细节就不会包含在生成的html中。生成html的外观与使用HiddenInput注解属性的情况相同,但是表单递交时就没有该属性的值被返回,这对模型绑定是有影响的。另外就是ScaffoldColumn注解属性对单属性辅助器不起作用,如果在视图中调用@Html.EditorFor(m=>m.PersonId),那么,即使有ScaffoldColumn注解属性存在,也会生成PersonId属性的编辑视图。
(2)使用用于标签的元数据
默认情况下,Label、LbaelFor、LabelForModel,以及EditorForModel辅助器以属性名作为它们生成的标签元素的内容(也就是生成html的label元素)。
例如,像下面这样渲染一个标签:
@Html.LabelFor(m=>m.BirthDate)
生成的html元素如下:
<label for="BirthDate">BirthDate</label>
当然,给属性定义的名字通常不是希望显示给用户的提示名字,为此可以使用DisplayName注解属性,例如:
[Display(Name="出生日期")] public DateTime BirthDate { get; set; }
当辅助器对BirthDate渲染html标签时,将Display注解属性,并用Name参数的值作为其内部文本,生成的html标签如下:
<div class="editor-label"><label for="BirthDate">出生日期</label></div> <div class="editor-field"><input class="text-box single-line" id="BirthDate" name="BirthDate" type="text" value="2014/6/12 0:00:00" /> </div>
另外,还可以对一个类加一个名字,这也是为了避免在前端hmtl中直接写关于一个类的名字,设想一下,如果这个类的名字信息修改了,那就要对每个写了这个类名字的前端页面逐一修改。我们可以采用DisplayName,来对一个类取显示名字,然后用@Html.LabelForModel()来显示这个名字。例如:
[DisplayName("人员信息")] public class Person { [HiddenInput(DisplayValue=false)] public int PersonId { get; set; } public string FirstName { get; set; } public string LastName { get; set; } [Display(Name="出生日期")] public DateTime BirthDate { get; set; } public Address HomeAddress { get; set; } public bool IsApproved { get; set; } public Role Role { get; set; } }
将Scaffold.cshtml修改为:
@model MvcApp.Models.Person @{ ViewBag.Title = "Scaffold"; } <h2>Person</h2> <h4>@Html.LabelForModel()</h4> @Html.EditorForModel()
注意,这里就用了@Html.LabelForModel()来显示了用DisplayName注解属性给类取的名字,显示效果如下:
@Html.LabelForModel()生成的html代码为:
<h4><label for="">人员信息</label></h4>
(3)使用用于数据值的元数据
我们也可以用元数据为如何显示一个模型属性提供一些指示,可以用这个办法解决出生日期属性包含时间的问题。需要使用的注解属性是DataType:
[DisplayName("人员信息")] public class Person { [HiddenInput(DisplayValue=false)] public int PersonId { get; set; } public string FirstName { get; set; } public string LastName { get; set; } [DataType(DataType.Date)] [Display(Name="出生日期")] public DateTime BirthDate { get; set; } public Address HomeAddress { get; set; } public bool IsApproved { get; set; } public Role Role { get; set; } }
DataType注解属性以DataType枚举中的一个值为参数,显示结果为:
DataType枚举中的一些常用值:
DateTime ——日期时间
Date ——日期
Time ——时间
Text ——显示单行文本
MultilineText ——将值渲染在一个文本区(textarea)元素中
Password ——以密码形式显示数据
Url ——将数据显示为一个url(用html的a标签)
EmailAddress ——将数据显示为一个e-mail地址(使用带有mailto的href的a标签)
注意这些值的效果依赖于它们所关联的属性类型,以及所使用的辅助器。例如,MultilineText值会让Editor辅助器创建一个html的文本区元素,但display辅助器对这个值是忽略的。同样,Url值只对display辅助器起作用,它渲染一个Html的a标签以创建一个链接。
(4)使用元数据选择显示模板
用显示模板来生成Html,使用UIHint注解属性来指定想用的模板,以渲染一个属性的html。例如:
[DisplayName("人员信息")] public class Person { [HiddenInput(DisplayValue=false)] public int PersonId { get; set; } [UIHint("MultilineText")] public string FirstName { get; set; } public string LastName { get; set; } [DataType(DataType.Date)] [Display(Name="出生日期")] public DateTime BirthDate { get; set; } public Address HomeAddress { get; set; } public bool IsApproved { get; set; } public Role Role { get; set; } }
这里对FirstName指定了显示模板[UIHint("MultilineText")],当与编辑辅助器(如EditorFor或EditorForModel)一起使用时,它会为FirstName属性渲染一个html多行文本框,如下显示:
常用的UIHint显示模板如下:(内建的mvc框架的视图模板)
Boolean——(Editor辅助器)渲染一个bool值的复选框。如果是nullable的bool?值,则渲染一个带有True、False、Not Set选项的select元素。(Display辅助器)与编辑辅助器相同,但附加了disable标签属性,使生成的html元素为只读。
Collection——(Editor辅助器)为IEnumerable序列中的每一个元素渲染一个相应的模板,该序列中的各个项不必是同种类型。(Display辅助器)与编辑辅助器相同。
Decimal——(Editor辅助器)渲染一个单行文本框的input元素,并对数据值格式化,显示两位小数。(Display辅助器)渲染格式化成两位小数的数据值。
EmailAddress——(Editor辅助器)将值渲染在一个单行文本框的input元素中。(Display辅助器)用html的a标签生成一个链接,且href标记属性格式化成一个mailto的url。
HiddenInput——(Editor辅助器)创建隐藏的input元素。(Display辅助器)与编辑辅助器相同。
Html——(Editor辅助器)将值渲染在一个单行文本框的input元素中。(Display辅助器)用html的a标签生成一个链接。
MultilineText——(Editor辅助器)渲染一个含有改数据值的html textarea元素。(Display辅助器)生成数据值。
Object
Password——(Editor辅助器)将值渲染在一个单行文本框的input元素中,不以明文显示,可以编辑。(Display辅助器)渲染数据值,字符是非隐蔽的。
String——(Editor辅助器)将值渲染在单行文本框的input元素中。(Display辅助器)渲染数据值。
Text——(Editor辅助器)等同于String模板。(Display辅助器)等同于String模板。
Url——(Editor辅助器)将值渲染在单行文本框的input元素中。
(5)把元数据运用于伙伴类(Buddy Class)
有些情况下不能直接在实体模型类型上使用元数据,如前面的[HiddenInput]、[HiddenInput(DisplayValue=false)]、[Display(Name="出生日期")]、 [DisplayName("人员信息")]、[DataType(DataType.Date)]、[UIHint("MultilineText")]等元数据。因为有些模型类是自动生成的,就像使用EF实体框架之类的ORM工具那样。对这种自动生成的类所做的任何修改,比如运用一些注解属性等,都会在工具对类的再次更新时丢失。
如果想用EF实体框架来自动生成模型类,又想在这些类上使用元数据,那就应该确保把这些模型类定义成分部类(partial类),并创建第二个分部类以包含这些元数据。许多自动生成类的工具默认情况下都是创建分部类,包括EF实体框架也是这样。例如,前面的Person例子,定义的时候像下面这样处理:
a.分部模型类
public partial class Person { public int PersonId { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public DateTime BirthDate { get; set; } public Address HomeAddress { get; set; } public bool IsApproved { get; set; } public Role Role { get; set; } }
由EF工具自动生成,或者自己建立,里面没有添加任何元数据。如果是EF自动建立,当EF对它进行更新时,也就是再次生成这个类时,在其上所做的修改都会丢失(比如加的注解属性等)。因此,我们创建第二个分部类,这让我们能够做一些保持附加信息的事情。当编译器建立应用程序时,这两个分部类将被合并起来。
b.定义元数据伙伴(第二个分部类,与Person要同名)
[MetadataType(typeof(PersonMetadataSource))] public partial class Person { }
分部类(partial clss)必须同名,而且要在同一个命名空间中用partial关键字声明。用于元数据目的的关键注解属性是[MetadataType(typeof(PersonMetadataSource))],参数中的PersonMetadataSource就是伙伴类(buddy class),通过把伙伴类的类型作为参数,让我们把伙伴类与Person类联系在一起。也就意味着Person类的元数据可以在名为PersonMetadataSource的伙伴类中找到,伙伴类定义如下
c.伙伴类
[DisplayName("人员信息")] class PersonMetadataSource { [HiddenInput(DisplayValue = false)] public int PersonId { get; set; } [UIHint("MultilineText")] public string FirstName { get; set; } [DataType(DataType.Date)] [Display(Name = "出生日期")] public DateTime BirthDate { get; set; } }
不用给全,只需要把带了元数据的属性给出就可以了。
执行后,显示效果,跟上一个例子一样。
5、使用复合类型参数
前面的例子中,在使用支架辅助器EditorForModel和DisplayForModel时,并未生成所有属性,例子中忽略了一个HomeAddress属性。发生这种情况是因为Object模板只对简单类型进行操作,这包括固有的c#类型,如int、bool、double等,还包括许多普通的框架类型,如Guid、DateTime等。
这就使得支架是非递归的,给定一个要处理的对象,支架模板视图辅助器将只生成简单属性类型的html,而会忽略本身是复合对象的任何属性。因此,如果要生成一个复合属性的html,我们必须明确地处理复合类属性。
可以用一个EditorForModel来处理我们视图模型对象的简单属性,然后用一个明确的Html.EditorFor调用来为HomeAddress属性(复合属性)生成html。而且,我们可以把各种元数据运用于Address类,就像在Person类上所做的那样。
修改Scaffold.cshtml如下:
@model MvcApp.Models.Person @{ ViewBag.Title = "Scaffold"; } <h2>Person</h2> <h4>@Html.LabelForModel()</h4> <div class="column"> @Html.EditorForModel() </div> <div class="column"> @Html.EditorFor(m=>m.HomeAddress) </div>
/Models/TestModelClass.cs内容如下:
namespace MvcApp.Models { public partial class Person { public int PersonId { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public DateTime BirthDate { get; set; } public Address HomeAddress { get; set; } public bool IsApproved { get; set; } public Role Role { get; set; } } [MetadataType(typeof(PersonMetadataSource))] public partial class Person { } [DisplayName("人员信息")] class PersonMetadataSource { [HiddenInput(DisplayValue = false)] public int PersonId { get; set; } //[UIHint("MultilineText")] public string FirstName { get; set; } [DataType(DataType.Date)] [Display(Name = "出生日期")] public DateTime BirthDate { get; set; } } public class Address { public string Line1 { get; set; } public string Line2 { get; set; } public string City { get; set; } public string PostalCode { get; set; } public string Country { get; set; } } public enum Role { Admin, User, Guest } }
HomeController.cs中的动作方法Scaffold代码如下:
public ViewResult Scaffold() { Person p1 = new Person { PersonId = 1, FirstName = "Joe", LastName = "Smith", BirthDate = DateTime.Parse("2014/6/12"), IsApproved = true, Role = Role.User }; return View(p1); }
在/Content/Site.css中增加样式如下:
/*Additional Styles 新增样式 ********************************************************/ .label { vertical-align:middle; } .check-box { vertical-align:bottom; margin: .5em 0 0 0; } div.column { float:left; width:auto; } .single-line { width: 100px; }
执行后的显示效果为:
二、定制模板视图辅助器系统
前面演示了如何用元数据来形成模板辅助器渲染数据的方式。但对于mvc框架,还有一些高级选项,能够让我们完全定制模板辅助器。
1、创建自定义编辑模板(Editor)
以Person类中的Role属性为例,这个属性的取值,是自定义的枚举类型Role中的某一个值,先看一下使用内建的模板辅助器对这个属性进行渲染的例子。假设还是在Scaffold.cshtml中添加了如下代码:
<p> @Html.LabelFor(m => m.Role): @Html.EditorFor(m => m.Role) </p> <p> @Html.LabelFor(m=>m.Role): @Html.DisplayFor(m=>m.Role) </p>
这一部分生成的显示效果为:
Label和Display的模板都可以符合要求,但是Editor模板生成的效果不太符合我们的要求。枚举类型Role中只定义了三个值(Admin、User、Guest),现在生成的编辑框允许用户给这个属性输入任意值。对于这样的情况我们就可以采用自定义编辑模板。步骤如下:
在/Views/Shared文件夹中新建一个名为EditroTemplates的文件夹,然后右击该文件夹,选择Add->View,为视图取名为Role,并选中Create as a partial view。再选中Create a strongly-typed view创建强类型视图,把Model class设置为Role。创建了这个分部视图后,就可以添加标准的Razor语法,以生成所需要的编辑视图。创建其html有很多方法,最简单的是混合使用静态html元素和Razor标签
@model MvcApp.Models.Role @using MvcApp.Models <select id="Role" name="Role"> @foreach (Role value in Enum.GetValues(typeof(Role))) { <option value="@value" @(Model == value ? "selected=\"selected\"" : "")> @value </option> } </select>
这个分部视图创建了一个html的select元素,并用Role枚举中的每个值填充它的option元素。在foreach循环中检查所传递的值是否与当前元素匹配,以便能正确设置selected属性。
当要生成这个属性的一个Editor视图时,这个分部视图就被用来生成html。例如Scaffold.cshtml
@model MvcApp.Models.Person @{ ViewBag.Title = "Scaffold"; } <p> @Html.LabelFor(m => m.Role): @Html.EditorFor(m => m.Role) </p> <p> @Html.LabelFor(m=>m.Role): @Html.DisplayFor(m=>m.Role) </p> <h2>Person</h2> <h4>@Html.LabelForModel()</h4> <div class="column"> @Html.EditorForModel() </div> <div class="column"> @Html.EditorFor(m=>m.HomeAddress) </div>
生成的效果为:
可以看到无论是使用的单个属性辅助器还是支架辅助器,它们都会查找并使用这个Role.cshtml模板。注意,模板那的名称(指Role.cshtml中的Role)对应于属性的类型而不是属性名,因此凡是使用了Role类型的属性都会运用这个自定义模板。
在这个例子中,包括文件夹的名字,以及文件名,都是要遵循的约定,不能随便乱取。Views/Shared/EditorTemplates文件夹,放在里面的Role.cshtml都是遵循约定。
Role.cshtml就是自定义模板(Template)。Role.cshtml模板之所以能够工作,是因为mvc框架在使用内建模板之前,会对一个给定的c#类型查找自定义模板。查找的顺序如下:
(1)传递给辅助器的模板——例如,Html.EditorFor(m=>m.SomeProperty, “MyTemplate”)将导致使用MyTemplate模板。
(2)由元数据注解属性指定的任意模板,如UIHint。
(3)与元数据指定的任意数据类型相关联的模板,如DataType注解属性。
(4)与待处理数据类型的.NET类名对应的任意模板。(与类型同名的模板)
(5)如果被处理的数据类型是一个简单类型,那么便采用内建的String模板。
(6)对应于数据类型基类的任意模板。
(7)如果数据类型实现IEnumerable,那么将使用内建的Collection模板。
(8)如果上述全部失败,将使用Object模板——服从于支架非递归规则。
在模板搜索的每一个阶段,mvc框架都会查找一个名为EditorTemplates/<name>或DisplayTemplates/<name>的模板。对于本例子中的Role.cshtml模板,满足上面顺序中的第4步,因为我们创建了一个名为Role.cshtml的模板,并把它放在了/Views/Shared/EditorTemplates文件夹中。
自定义模板是用搜索常规视图的同样方式来查找的,这意味着我们可以创建一个控制器专用的自定义模板,并把它放在/Views/<controller>/EditorTemplates文件夹中,以覆盖在/Views/Shared文件夹中找到的模板。
2、创建自定义显示模板(display)
过程类似于创建自定义编辑模板,只不过自定义显示模板被放在DisplayTemplates文件夹中。下面的例子演示了一个Role枚举的自定义显示模板,把这个模板文件创建为/Views/Shared/DisplayTemplates/Role.cshtml
@model MvcApp.Models.Role @using MvcApp.Models @foreach (Role value in Enum.GetValues(typeof(Role))) { if (value == Model) { <b>@value</b> } else { @value } }
这个模板列举枚举Role中的所有值,如果与传递给当前视图的数据值相等,那就用加粗显示。
例如,如果在Scaffold.cshtml中有使用Display辅助器:
<p> @Html.LabelFor(m => m.Role): @Html.EditorFor(m => m.Role) </p> <p> @Html.LabelFor(m=>m.Role): @Html.DisplayFor(m=>m.Role) </p>
那么执行后的显示结果为:
可以看到在Display辅助器对应的地方,当前数据被加粗了。
3、创建泛型模板
不光是可以创建类型专用的模板,例如,还可以创建一个工作与所有枚举的模板,并且随后指定用UIHint注解属性来选择这个模板。根据前面的模板搜索顺序,可以看到用UIHint注解属性指定的模板优先于类型专用模板。
下面在/Views/Shared/EditorTemplates文件夹中新建Enum.cshtml模板,这个模板是对c#枚举的一个更通用的处理。
@model Enum @Html.DropDownListFor(m=>m, Enum.GetValues(Model.GetType()) .Cast<Enum>() .Select(m=>{ string enumVal=Enum.GetName(Model.GetType(),m); return new SelectListItem(){ Selected = (Model.ToString()==enumVal), Text=enumVal, Value=enumVal }; } ))
该模板的视图模型是Enum,它可以枚举任何类型。可以再次混用静态html和Razor语法来创建这个模板,但在这个例子中使用了强类型的DropDownListFor辅助器,并使用了一些LINQ来把枚举值转换成SelectListItems。
使用的时候,就用:
[UIHint("Enum")] public Role Role { get; set; }
4、替换内建模板
如果创建一个与内建模板同名的自定义模板,mvc框架将优先于内建模板来使用这个自定义版本。
比如,对于bool类型的IsApproved属性
public partial class Person { public int PersonId { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public DateTime BirthDate { get; set; } public Address HomeAddress { get; set; } public bool IsApproved { get; set; } public Role Role { get; set; } } [MetadataType(typeof(PersonMetadataSource))] public partial class Person { } [DisplayName("人员信息")] class PersonMetadataSource { [HiddenInput(DisplayValue = false)] public int PersonId { get; set; } //[UIHint("MultilineText")] public string FirstName { get; set; } [DataType(DataType.Date)] [Display(Name = "出生日期")] public DateTime BirthDate { get; set; } //[UIHint("Enum")] public Role Role { get; set; } }
HomeController里的Scaffold动作方法为:
public ViewResult Scaffold() { Person p1 = new Person { PersonId = 1, FirstName = "Joe", LastName = "Smith", BirthDate = DateTime.Parse("2014/6/12"), IsApproved = true, Role = Role.User }; return View(p1); }
如果使用了Display辅助器,来显示bool类型的IsApproved属性
<p> @Html.LabelFor(m=>m.IsApproved): @Html.DisplayFor(m=>m.IsApproved) </p>
显示效果为:
因为用的是Display辅助器,所以不能编辑,以灰色显示。
现在自定义内建模板Boolean,要在Display辅助器中起效果,把它放在/Views/Shared/DisplayTemplates/Boolean.cshtml中。
@model bool? @if (ViewData.ModelMetadata.IsNullableValueType && Model == null) { @:True False <b>Not Set</b> } else if (Model.Value) { @:<b>True</b> False Not Set } else { @:True <b>False</b> Not Set }
显示结果为:
5、使用ViewData.TemplateInfo属性
mvc提供ViewData.TemplateInfo属性,使得编写自定义视图模板更容易。该属性返回一个TemplateInfo对象。
这个类的一些常用成员:
FormattedModelValue——返回当前模型的字符串表示,所返回的字符串考虑了格式化元数据(如DataType注解属性等)。
GetFullHtmlFieldId()——返回可以用于html的id标签属性的字符串。
GetFullHtmlFieldName()——返回可以用于html的name标签属性的字符串。
HtmlFieldPrefix——返回字段前缀
(1)关注数据格式化
下面举一个例子,使Model中能使用这个placeholder,在编辑框上给出提示信息。
首先,我们要新建一个PlaceHolderAttribute类,让它继承Attribute, IMetadataAware两个接口。在解决方案管理器中新建一个文件夹Infrastructures,在文件夹里新建类库文件PlaceHolderAttribute.cs
namespace MvcApp.Infrastructures { public class PlaceHolderAttribute : Attribute, IMetadataAware { private readonly string _placeholder; public PlaceHolderAttribute(string placeholder) { _placeholder = placeholder; } public void OnMetadataCreated(ModelMetadata metadata) { metadata.AdditionalValues["placeholder"] = _placeholder; } } }
再自定义一个PlaceHolder的显示模板,一般用文本框接收的数据通常都是string类型的,所以针对string类型,编写类型的自定义模板,所以以类型名string为文件名,创建类型的自定义模板,位置在/Views/Shared/EditorTemplates/string.cshtml
@{ var placeholder = string.Empty; if (ViewData.ModelMetadata.AdditionalValues.ContainsKey("placeholder")) { placeholder = ViewData.ModelMetadata.AdditionalValues["placeholder"] as string; } } @Html.TextBox("", ViewData.TemplateInfo.FormattedModelValue, new { placeholder = placeholder })
接下来就可以在模型类上使用我们自定义的PlaceHolder类(当然,全称是PlaceHolderAttribute累,使用时只用Attribute之前的名字)了,例如:
public partial class Person { public int PersonId { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public DateTime BirthDate { get; set; } public Address HomeAddress { get; set; } public bool IsApproved { get; set; } public Role Role { get; set; } } [MetadataType(typeof(PersonMetadataSource))] public partial class Person { } [DisplayName("人员信息")] class PersonMetadataSource { [HiddenInput(DisplayValue = false)] public int PersonId { get; set; } //[UIHint("MultilineText")] [PlaceHolder("firstname")] public string FirstName { get; set; } [DataType(DataType.Date)] [Display(Name = "出生日期")] public DateTime BirthDate { get; set; } //[UIHint("Enum")] public Role Role { get; set; } }
这个例子中,只对FirstName使用了我们自定义的PlaceHolder类
[PlaceHolder("firstname")]
public string FirstName { get; set; }
当用 @Html.EditorFor(m=>m.FirstName)来生成FirstName属性时,如果文本框中没有内容,或者有内容时把内容删除掉,那么PlaceHolder的提示信息就会显示出来,以灰色显示。
在这个例子中,我们定义了string.cshtml后,导致css格式发生了错位,所以右边第二列有错位重叠的现象。
(2)使用html前缀
当渲染一个有层次结构的视图时,mvc框架会跟踪正在渲染的属性名,并通过TemplateInfo对象的HtmlFieldPrefix属性,为我们提供一个唯一的参考点。例如Person类的HomeAddress属性,这是一个Address对象。如果像下面这样渲染一个属性的模板:
@Html.EditorFor(m=>m.HomeAddress.PostalCode)
那么,传递给这个模板的HtmlFieldPrefix的值将是HomeAddress.PostalCode。HtmlFieldPrefix属性所渲染的值通常不能直接作为html标签属性的值,因此TemplateInfo对象包含了GetFullHtmlFieldId方法和GetFullHtmlFieldName方法,以便把这个唯一的标识转换成可用的东西。
lyj