在 ASP.NET MVC中,定制控件

分享于 

21分钟阅读

Web开发

  繁體

介绍

我是 Esendex的软件开发人员,业务短信服务提供商。 我们目前开发一个新版本的网络应用,由我们的客户用来发送和接收短信。 这是 ASP.NET MVC中的驱动力 behind 自定义控件- 我们对 HtmlHelper 方法不满意,并且找不到完全可以用的替代。 项目"Doyle"是我们的第一个 ASP.NET 应用程序,所以我们希望将一些基础。 我们最初的目标是设计一个新的页面来撰写和发送消息。 这由一个具有三个输入的窗体组成: 收件人,发起人和 body 我们还希望对每个输入和服务器端验证都有水印效果。 创建控制库将允许我们在许多项目中使用额外的功能,并简化开发过程。

背景

ASP.NET MVC是开发web应用程序的一种相对。 虽然项目仍然在测试中,但是许多人开始意识到潜在的潜力,并在传统的web表单。 我不会讨论他们之间的优缺点,但我的第一次遇到的是正确的。 我猜,这两种风格都会存在,而不是一个变得越来越优势。

我假设你阅读本文,你已经了解了 MVC Pattern,因这里我不会在这里进行太多细节。 基本上,它将应用程序分为三个层次: 模型。视图和控制器。模型包含业务逻辑和对象,视图呈现用户界面( 用户界面),控制器处理用户交互。 控制器是模型和视图之间的链接,并且应该非常离散。 始终记住:瘦控制器,fat模型。

如果过去使用过web表单,你将知道创建自定义控件非常容易。 微软为你提供了标准HTML元素的简单包装器。 一些更高级的工具包括用于导航。验证和数据表示的工具。 不管它的复杂性如何,你都可以使用继承来创建新控件。 通过这样做,他们自动获取基本控件的特性,让你集中于新的东西。 例如,如果希望一组文本框具有某一特定功能,则可以从 System.Web.UI.WebControls.TextBox 中进行 inherit 并实现修改:

namespace Esendex.CustomControls
{
 publicclass MyTextBox : System.Web.UI.WebControls.TextBox
 {
 // Modifications go here... }
}

之后,在 Web.config file: 中,register的名称空间

<pages><controls><addtagPrefix="cc"assembly=" 
 Esendex"namespace="Esendex.CustomControls"/></controls></pages>

最后,将所需的控件插入到页面中:

<cc:MyTextBoxrunat="server"/>

由于 ASP.NET MVC不禁止使用服务器或者用户控件,但是它对它们 frown - 因为web页面( 或者视图) 遵循不同的生命周期,所以不能正常工作。 为此,一些替换了 UI助手 - - 返回 HTML Fragment的方法。 这些附加到 static 类 HtmlHelper,可以从任何视图访问它们:

<%= Html.TextBox("name", "Please enter your name...")%>

输出:

<inputid="name"name="name"type="text"value="Please enter your name..."/>

我们会注意到内联代码用于呈现返回字符串- - 我们一直在尝试摆脱的一些东西。 首先,你可能对这个概念有印象,但它很快就会变得烦人。 例如,TextBox() 方法有两个可选的第三个参数,这两个参数都表示附加的HTML属性( 前两个分别用于 NAME 和值)。 响铃应该在这里时响铃,问题需要问,如何更改CSS类或者设置列的数量。 答案是将匿名类型与第三个参数结合使用:

<%= Html.TextBox("name", "Please enter your name...", 
 new { @class = "styledInput", size = 50 })%>

输出:

<inputclass="styledInput"id="name"name="name"size="50"type="text"value="Please enter your name..."/>

这个问题的第一个问题是你必须记住属性名- 这是正确的,没有智能感知 ! 保留的关键字等" "也 annoying - 它们必须用"@"前缀。 使用匿名类型使得整个过程变得更加直观,对于那些新的面向MVC的人来说,这是更加复杂的。

一个伟大的想法

如果希望扩展现有的helper 方法,该怎么办? 简单的答案是创建一个重载,甚至是一个新的方法,如预期的那样返回一些。 尝试这样做,我保证你会失去一定量的重载。 只要看一下 ASP.NET MVC源代码,如果你不相信我的话。

我提出了一个控制库,扩展了Handley的思想,从而摆脱了传统 HtmlHelper 方法的束缚。 他写了一篇文章给我灵感,但是他的示例只包含了一个文本框实现,缺少一些基本功能。 总的来说,他创建了一个基类,每个控件都将为 inherit 添加可以重用的属性/方法。 然后 ToString() 方法被重写为( 在任何必要的级别) 并设计为返回 HTML。 我最喜欢的是在视图中实例化控件的方式:

<%= new MvcTextBox() { Name = "name" }%>

注意构造函数没有任何参数。 相反,它利用对象初始化器( ),允许在创建时分配字段/属性。 .NET 中的这个新特性为初始化对象提供了完整的智能感知,具有能够只指定重要值的灵活性,而无需指定重要的值。

拿点东西让它更好

我的方法基于jeff的风格,但我做了一些改进。 下面是每个控件继承的基类:

publicabstractclass MvcControl
{
 protected IDictionary<string, string> Attributes { get; privateset; }
 publicstring Class
 {
 set { AddClass(value); }
 }
 publicvirtualstring ID
 {
 get { return Attributes.GetValue("id"); }
 set { Attributes.Merge("id", value); }
 }
 protectedstring InnerHtml { get; set; }
 publicobject HtmlAttributes { get; set; }
 publicstring Style
 {
 set { Attributes.Merge("style", value); }
 }
 privatestring TagName { get; set; }
 private TagRenderMode TagRenderMode { get; set; }
 publicstring Title
 {
 set { Attributes.Merge("title", value); }
 }
 public MvcControl(string tagName)
 : this(tagName, TagRenderMode.Normal) { }
 public MvcControl(string tagName, TagRenderMode tagRenderMode)
 {
 Attributes = new SortedDictionary<string, 
 string>(StringComparer.Ordinal);
 TagName = tagName;
 TagRenderMode = tagRenderMode;
 }
 publicvoid AddClass(string className)
 {
 if (string.IsNullOrEmpty(className))
 {
 className = className.Trim();
 }
 string currentClassName;
 if (Attributes.TryGetValue("class", out currentClassName))
 {
 currentClassName = currentClassName.Trim();
 Attributes["class"] = currentClassName + 
 "" + className;
 }
 else {
 Attributes["class"] = className;
 }
 }
 publicvoid AddEventScript(string eventKey, string script)
 {
 string newScript = script;
 if (string.IsNullOrEmpty(newScript))
 {
 newScript = newScript.Trim();
 if (!newScript.EndsWith("}")
 &&!newScript.EndsWith(";"))
 {
 newScript += ";";
 }
 }
 string currentScript;
 if (Attributes.TryGetValue(eventKey, out currentScript))
 {
 currentScript = currentScript.Trim();
 if (!currentScript.EndsWith("}")
 &&!currentScript.EndsWith(";"))
 {
 currentScript += ";";
 }
 Attributes[eventKey] = currentScript + "" + newScript;
 }
 else {
 Attributes[eventKey] = newScript;
 }
 }
 private TagBuilder GetTagBuilder()
 {
 TagBuilder tagBuilder = new TagBuilder(TagName);
 tagBuilder.MergeAttributes(new RouteValueDictionary(HtmlAttributes));
 tagBuilder.MergeAttributes(Attributes);
 tagBuilder.InnerHtml = InnerHtml;
 return tagBuilder;
 }
 publicstring Html(ViewContext viewContext)
 {
 if (viewContext == null)
 {
 thrownew ArgumentNullException("viewContext");
 }
 StringBuilder html = new StringBuilder();
 Initialise(viewContext);
 TagBuilder tagBuilder = GetTagBuilder();
 using (StringWriter writer = new StringWriter(html))
 {
 writer.Write(tagBuilder.ToString(TagRenderMode));
 RenderCustomHtml(writer, viewContext);
 }
 return html.ToString();
 }
 protectedvirtualvoid Initialise(ViewContext viewContext) { }
 protectedvirtualvoid RenderCustomHtml(StringWriter writer, 
 ViewContext viewContext) { }
 protectedvoid SetInnerText(object innerText)
 {
 if (innerText == null)
 {
 SetInnerText(null);
 }
 SetInnerText(innerText.ToString());
 }
 protectedvoid SetInnerText(string innerText)
 {
 InnerHtml = HttpUtility.HtmlEncode(innerText);
 }
}

我添加了几个 public 属性映射到存储在 IDictionary 集合中的HTML属性。 IDClassStyleTitle 可以应用于任何HTML元素,因这里它们可以在基类中实现。 没有必要有 获取 访问器,因为开发人员应该只设置这些值。

如果控件需要访问 ViewContext 信息,请重写 Initialise() 方法。 某些控件可能还需要呈现附加的HTML ( 比如,MvcCheckBox )。 重写 RenderHtml() 方法提供了对 <code>StringWriter的访问,该可以相应地追加。

下面是一个呈现HTML标签元素的控件:

publicclass MvcLabel : MvcControl
{
 protectedstring AssociatedControlID
 {
 get { return Attributes["for"]; }
 privateset { Attributes["for"] = value; }
 }
 protectedstring Text
 {
 get { return InnerHtml; }
 privateset { InnerHtml = value; }
 }
 public MvcLabel(string associatedControlID, string text)
 : base("label")
 {
 AssociatedControlID = associatedControlID;
 Text = text;
 }
}

这个 上面 定义非常小,但是最终结果仍然令人印象深刻。 为了扩展 MvcControl,我添加了两个属性: 在构造函数中指定这些选项( 这有助于确保输出HTML对最小规范( 带有一个带一个标签的普通 LABEL 标记) 有效。 for 属性和一些内部 HTML )。

可以使用以下 HtmlHelper 扩展方法将控件添加到视图中,该方法接受 MvcControl的实例:

publicstaticstring MvcControl(this HtmlHelper htmlHelper, MvcControl mvcControl)
{
 if (mvcControl == null)
 {
 thrownew ArgumentNullException("mvcControl");
 }
 return mvcControl.Html(htmlHelper.ViewContext);
}

以下是将 MvcLabel 添加到视图的方法:

<%= Html.MvcControl(new MvcLabel("name", "Name"))%>

输出:

<labelfor="name">Name</label>

对于更复杂的场景,你可能需要指定类和标题。 幸运的是,因为 MvcLabel 继承了 MvcControl,所以你可以免费获得这里功能:

<%= Html.MvcControl(new MvcLabel("name", "Name")
 { Class = "inputHeading", Title = "Name" })%>

输出:

<labelclass="inputHeading"for="name"title="Name">Name</label>

你可能会认为,实例化这些控件更加复杂。 我承认早期的尝试是更简单的,但我很快发现了来自当前请求的信息的需求。 对于相应的控制和更改行为或者从模型中填充数据的控件来说,ViewContextViewData 非常有用。 两者都可以从视图中访问,但我不想手动处理它们。 作为解决方法,ViewContext 会自动从 HtmlHelper inside 分配扩展方法。

我还想将开发人员限制为一种实例化控件的单一方法,HtmlHelper 方法是作业( 几乎)。 我的预测是大多数人都会选择简单的方法,但是有更复杂的替代方法:

<%= new MvcLabel("name", "Name")
 { Class = "inputHeading", Title = "Name" }.Html(ViewContext)%>

Html() 方法将引发异常,如果 通过,因此建议使用 HtmlHelper 方法保持一致性,减少机会或者错误。

最后的机会

为了完全比较这两个实现,我认为我只能展示我的MvcTextBox 版本。 包括文本框在内的许多表单元素都是基于 INPUT 标签的- differentiator是 type 属性。 为此,我创建了另一个基类来封装基本功能:

publicabstractclass MvcInput : MvcEventAttributes
{
 protectedoverridevoid Initialise(ViewContext viewContext)
 {
 if (viewContext == null)
 {
 thrownew ArgumentNullException("viewContext");
 }
 ViewDataDictionary viewData = viewContext.ViewData;
 if (viewData == null)
 {
 thrownew ArgumentNullException("viewData");
 }
 string attemptedValue = viewData.GetModelAttemptedValue(Name);
 if (Type == InputType.CheckBox)
 {
 if (!string.IsNullOrEmpty(attemptedValue))
 {
 bool isChecked;
 string[] attemptedValues = attemptedValue.Split(',');
 if (bool.TryParse(attemptedValues[0], out isChecked))
 {
 if (isChecked)
 {
 Attributes["checked"] = "checked";
 }
 else {
 Attributes.Remove("checked");
 }
 }
 }
 }
 elseif (Type == InputType.RadioButton)
 {
 if (!string.IsNullOrEmpty(attemptedValue))
 {
 stringvalue = Attributes.GetValue("value");
 if (value.Equals(attemptedValue, 
 StringComparison.InvariantCultureIgnoreCase))
 {
 Attributes["checked"] = "checked";
 }
 else {
 Attributes.Remove("checked");
 }
 }
 }
 elseif (Type!= InputType.File)
 {
 if (attemptedValue!= null)
 {
 Attributes["value"] = attemptedValue;
 }
 elseif (viewData[Name]!= null)
 {
 Attributes["value"] = viewData.EvalString(Name);
 } 
}
 ModelState modelState;
 if (viewData.ModelState.TryGetValue(Name, out modelState))
 {
 if (modelState.Errors.Count >0)
 {
 AddClass(InvalidCssClass);
 }
 }
 }
 protectedstring Name
 {
 get { return Attributes.GetValue("name"); }
 privateset { Attributes.Merge("name", value); }
 }
 publicstring InvalidCssClass { get; set; }
 protectedvirtualbool IsIDRequired
 {
 get { return Type!= InputType.RadioButton; }
 }
 protected InputType Type
 {
 get { return MvcControlHelper.GetInputTypeEnum(Attributes.GetValue("type")); }
 set { Attributes["type"] = MvcControlHelper.GetInputTypeString(value); }
 }
 public MvcInput(InputType type, string name)
 : base("input", TagRenderMode.SelfClosing)
 {
 if (string.IsNullOrEmpty(name))
 {
 thrownew ArgumentException("Value cannot be null or empty.", "name");
 }
 Type = type;
 if (IsIDRequired)
 {
 ID = name;
 }
 InvalidCssClass = "input-validation-error";
 Name = name;
 }
}

MvcInput 类处理表单验证,并使用从模型传递的数据设置默认值。 默认情况下,无效元素的CSS类是" input-validation-error",但你可以指定自定义值。

MvcTextBox的唯一增强是一个水印功能,它设置文本框的值并在元素获得焦点( 若要启用这里效果,视图必须引用 Watermark.js。) 时清除它:

publicclass MvcTextBox : MvcInput
{
 protectedoverridevoid RenderHtml(StringWriter writer, 
 ViewContext viewContext)
 {
 if (writer == null)
 {
 thrownew ArgumentNullException("writer");
 }
 if (viewContext == null)
 {
 thrownew ArgumentNullException("viewContext");
 }
 MvcControlHelper.RenderWatermarkScript(writer, viewContext, 
 ID, Name, WatermarkedCssClass, WatermarkText);
 }
 publicint Columns
 {
 set { Attributes.Merge("size", value.ToString()); }
 }
 publicstring MaximumLength
 {
 set { Attributes.Merge("maxlength", value); }
 }
 publicstring WatermarkedCssClass { get; set; }
 publicstring WatermarkText { get; set; }
 publicobject Value
 {
 set { Attributes.Merge("value", value); }
 }
 public MvcTextBox(string name)
 : base(InputType.Text, name)
 {
 WatermarkedCssClass = "input-watermarked";
 }
}

下面是渲染 MvcTextBox的代码:

<%= Html.MvcControl(new MvcTextBox("name")
 { Columns = 50, Class = "styledInput", Value = "Please enter your name..." })%>

输出:

<inputclass="styledInput"id="name"name="name"size="50"type="text"value="Please enter your name..."/>

jeff相比,可以能有更多的字符,但是计划是使编码更容易。 我想知道最好的方法的唯一方法是同时尝试它们。

摘要

为了将这一切放到上下文中,源代码演示如何创建应用程序以发送SMS消息。 默认视图包含用于捕获所有必需信息( 传递给方法的所有必需信息)的表单,其中进行基本验证。 文件的范围可以链接到我们的sdk/api中,这意味着消息将被传递- 注册一个免费试用程序。

我还没有完成这个库,但它是一个很好的起点。 希望在阅读本文之后,你将了解今天花费额外时间来产生某些东西的好处。

我们的目标是让Doyle在 2009的第一季度准备 public 测试。 按照计划,所有视图都使用MvcControls库,我们对结果非常满意。 这是我们撰写页面的预览:

* 为了运行演示,你需要安装 .NET Framework和微软 ASP.NET MVC测试版

确认

一些代码Fragment是从微软 ASP.NET 测试版源复制的。 特别是,UrlHelperExtensions 只是公开了已经定义过的方法 内部 关键字。

我想再次感谢 Jeff Handley的原始想法。