ASP.NET MVC 自定义控件






4.76/5 (31投票s)
用于在 ASP.NET MVC 应用程序中渲染自定义 HTML 的控件库。
引言
我是一名软件开发人员,在商务短信服务提供商 Esendex 工作。我们目前正在开发我们客户用来发送和接收短信的新版 Web 应用程序。这是 ASP.NET MVC 自定义控件的驱动力——我们对 HtmlHelper
方法不满意,也找不到一个功能齐全的替代方案。项目“Doyle”是我们第一个 ASP.NET MVC 应用程序,所以我们想打下一些基础。我们最初的目标是设计一个新的页面来撰写和发送消息。这包括一个包含三个输入的表单:收件人、发件人和正文。我们还希望每个输入都有水印效果和服务器端验证。创建控件库将使我们能够在众多项目中重用额外的功能并简化开发过程。
背景
ASP.NET MVC 是一种相对较新的 Web 应用程序开发方法。尽管该项目仍处于 beta 阶段,但许多人已开始认识到其潜力,并将其置于传统 Web Forms 之上。我不会深入探讨它们之间的优缺点,但我的初步接触是积极的。我猜这两种风格都会并存,而不是其中一种变得更占主导地位——拥有选择总是好的,而且两者都有不同的东西可以提供。
我假设您正在阅读本文,您已经理解了 MVC 模式,所以我不会在这里做太多详细介绍。基本上,它将应用程序分为三个层:模型、视图和控制器。模型包含业务逻辑和对象,视图渲染用户界面 (UI),控制器处理用户交互。控制器是模型和视图之间的链接,并且应该非常离散。永远记住:瘦控制器,胖模型。
如果您过去使用过 Web Forms,您会知道创建自定义控件非常容易。开箱即用,Microsoft 为标准 HTML 元素提供了简单的包装器。其中一些更高级的包括导航、验证和数据表示的工具。无论其复杂程度如何,您都可以通过继承来创建新控件。通过这样做,它们会自动获得基控件的特性,让您可以专注于新内容。例如,如果您希望一系列文本框具有某个特定功能,您可以继承自 System.Web.UI.WebControls.TextBox
并实现修改。
namespace Esendex.CustomControls
{
public class MyTextBox : System.Web.UI.WebControls.TextBox
{
// Modifications go here...
}
}
之后,在Web.config文件中注册控件的命名空间
<pages>
<controls>
<add tagPrefix="cc" assembly="
Esendex" namespace="Esendex.CustomControls" />
</controls>
</pages>
最后,将所需的控件插入到您的页面中
<cc:MyTextBox runat="server" />
ASP.NET MVC 不禁止使用服务器控件或用户控件,但它不赞成使用它们——许多控件将无法正常工作,因为 Web 页面(或视图)遵循不同的生命周期。为了弥补这一点,一些控件已被 UI 助手 取代——这些助手是返回 HTML 片段的方法。它们附加到一个静态类 HtmlHelper
,该类可从任何视图访问。
<%= Html.TextBox("name", "Please enter your name...")%>
输出
<input id="name" name="name" type="text" value="Please enter your name..." />
您会注意到内联代码用于渲染返回字符串——这是我们多年来一直在努力避免的事情。一开始,您可能会对这个概念印象深刻,但很快就会变得令人厌烦。例如,TextBox()
方法有两个可选的第三个参数,它们都代表额外的 HTML 属性(前两个分别代表名称和值)。此时应该响起警报,并需要提出问题;如何更改 CSS 类或设置列数?答案是使用匿名类型结合第三个参数。
<%= Html.TextBox("name", "Please enter your name...",
new { @class = "styledInput", size = 50 })%>
输出
<input class="styledInput" id="name" name="name"
size="50" type="text" value="Please enter your name..." />
我对这个问题的第一个问题是,您必须记住属性名称——没错,没有智能感知!像“class
”这样的保留关键字也很烦人——它们必须以“@”为前缀。使用匿名类型只会使整个过程对 ASP.NET MVC 的新手来说不那么直观,而且更复杂。
一个绝妙的主意
如果您想扩展现有的辅助方法该怎么办?简单的答案是创建一个重载方法,甚至是一个新方法,以返回预期的漂亮 HTML。尝试这样做,我保证您会厌烦不断出现的大量重载。如果您不相信我,只需查看 ASP.NET MVC 源代码即可。
为了摆脱传统的 HtmlHelper
方法,我想出了一个控件库,它扩展了 Jeff Handley 的想法。他写了一篇 文章,给了我灵感,但他的示例只包含了一个文本框实现,缺少一些基本功能。总而言之,他创建了一个基类,所有控件都将继承它——可重用的属性/方法尽可能低地添加,以最大程度地减少代码重复。然后重写 ToString()
方法(在任何必要的级别)并设计为返回 HTML。我最喜欢的是在视图中实例化控件的方式。
<%= new MvcTextBox() { Name = "name" }%>
请注意,构造函数没有任何参数。相反,它利用了 对象初始化程序,它允许您在创建时分配字段/属性。 .NET 3.5 中的这项新功能为您提供了正在初始化的对象的完整智能感知,同时能够只指定重要值的灵活性——没有一个是强制性的。
拿来一些东西,让它变得更好
我的方法在很大程度上基于 Jeff 的风格,但我做了一些改进。这是每个控件都继承的基类。
public abstract class MvcControl
{
protected IDictionary<string, string> Attributes { get; private set; }
public string Class
{
set { AddClass(value); }
}
public virtual string ID
{
get { return Attributes.GetValue("id"); }
set { Attributes.Merge("id", value); }
}
protected string InnerHtml { get; set; }
public object HtmlAttributes { get; set; }
public string Style
{
set { Attributes.Merge("style", value); }
}
private string TagName { get; set; }
private TagRenderMode TagRenderMode { get; set; }
public string 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;
}
public void 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;
}
}
public void 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;
}
public string Html(ViewContext viewContext)
{
if (viewContext == null)
{
throw new 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();
}
protected virtual void Initialise(ViewContext viewContext) { }
protected virtual void RenderCustomHtml(StringWriter writer,
ViewContext viewContext) { }
protected void SetInnerText(object innerText)
{
if (innerText == null)
{
SetInnerText(null);
}
SetInnerText(innerText.ToString());
}
protected void SetInnerText(string innerText)
{
InnerHtml = HttpUtility.HtmlEncode(innerText);
}
}
我添加了几个公共属性,这些属性映射到存储在 IDictionary
集合中的 HTML 属性。ID
、Class
、Style
和 Title
可以应用于任何 HTML 元素,因此将它们放在基类中是有意义的。不需要有 get
访问器,因为开发人员只应该设置这些值。
如果控件需要访问 ViewContext
信息,请重写 Initialise()
方法。一些控件也可能需要渲染额外的 HTML(例如,MvcCheckBox
)。重写 RenderHtml()
方法可以访问 StringWriter
,可以相应地进行附加。
这是一个渲染 HTML 标签元素的控件。
public class MvcLabel : MvcControl
{
protected string AssociatedControlID
{
get { return Attributes["for"]; }
private set { Attributes["for"] = value; }
}
protected string Text
{
get { return InnerHtml; }
private set { InnerHtml = value; }
}
public MvcLabel(string associatedControlID, string text)
: base("label")
{
AssociatedControlID = associatedControlID;
Text = text;
}
}
上面的定义非常小,但最终结果仍然令人印象深刻。为了扩展 MvcControl
,我添加了两个属性:AssociatedControlID
和 Text
。您会注意到这些在构造函数中被赋值——这有助于确保输出的 HTML 在最小规范(一个带有 for
属性和一些内部 HTML 的普通 LABEL
标签)上是有效的。
可以使用以下接受 MvcControl
实例的 HtmlHelper
扩展方法将控件添加到视图中。
public static string MvcControl(this HtmlHelper htmlHelper, MvcControl mvcControl)
{
if (mvcControl == null)
{
throw new ArgumentNullException("mvcControl");
}
return mvcControl.Html(htmlHelper.ViewContext);
}
这是如何将 MvcLabel
添加到视图中。
<%= Html.MvcControl(new MvcLabel("name", "Name"))%>
输出
<label for="name">Name</label>
对于更复杂的场景,您可能希望指定类和标题。幸运的是,您免费获得了此功能,因为 MvcLabel
继承自 MvcControl
。
<%= Html.MvcControl(new MvcLabel("name", "Name")
{ Class = "inputHeading", Title = "Name" })%>
输出
<label class="inputHeading" for="name" title="Name">Name</label>
您可能认为实例化这些控件更复杂。我承认我早期的尝试更简单,但我很快发现需要当前请求的信息,并想出了一个有效的解决方案。ViewContext
和 ViewData
对于处理验证并相应地更改行为或从模型填充数据的控件很有用。两者都可以从视图访问,但我不想手动处理它们。作为一种变通方法,ViewContext
在扩展方法内的 HtmlHelper
中自动分配。
我还想限制开发人员以单一方式实例化控件,而 HtmlHelper
方法(几乎)做到了这一点。我的预测是,大多数人会选择简单的方法,但还有一个更复杂的替代方案。
<%= new MvcLabel("name", "Name")
{ Class = "inputHeading", Title = "Name" }.Html(ViewContext)%>
如果传递 null
值,Html()
方法将抛出异常,因此我建议使用 HtmlHelper
方法来保持一致性并降低出错的可能性。
给您留下深刻印象的最后机会
为了全面比较这两种实现,我认为展示我版本的 MvcTextBox
是公平的。许多表单元素,包括文本框,都基于 INPUT
标签——区别在于 type
属性。因此,我创建了另一个基类来封装基本功能。
public abstract class MvcInput : MvcEventAttributes
{
protected override void Initialise(ViewContext viewContext)
{
if (viewContext == null)
{
throw new ArgumentNullException("viewContext");
}
ViewDataDictionary viewData = viewContext.ViewData;
if (viewData == null)
{
throw new 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");
}
}
}
}
else if (Type == InputType.RadioButton)
{
if (!string.IsNullOrEmpty(attemptedValue))
{
string value = Attributes.GetValue("value");
if (value.Equals(attemptedValue,
StringComparison.InvariantCultureIgnoreCase))
{
Attributes["checked"] = "checked";
}
else
{
Attributes.Remove("checked");
}
}
}
else if (Type != InputType.File)
{
if (attemptedValue != null)
{
Attributes["value"] = attemptedValue;
}
else if (viewData[Name] != null)
{
Attributes["value"] = viewData.EvalString(Name);
}
}
ModelState modelState;
if (viewData.ModelState.TryGetValue(Name, out modelState))
{
if (modelState.Errors.Count > 0)
{
AddClass(InvalidCssClass);
}
}
}
protected string Name
{
get { return Attributes.GetValue("name"); }
private set { Attributes.Merge("name", value); }
}
public string InvalidCssClass { get; set; }
protected virtual bool 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))
{
throw new 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)。
public class MvcTextBox : MvcInput
{
protected override void RenderHtml(StringWriter writer,
ViewContext viewContext)
{
if (writer == null)
{
throw new ArgumentNullException("writer");
}
if (viewContext == null)
{
throw new ArgumentNullException("viewContext");
}
MvcControlHelper.RenderWatermarkScript(writer, viewContext,
ID, Name, WatermarkedCssClass, WatermarkText);
}
public int Columns
{
set { Attributes.Merge("size", value.ToString()); }
}
public string MaximumLength
{
set { Attributes.Merge("maxlength", value); }
}
public string WatermarkedCssClass { get; set; }
public string WatermarkText { get; set; }
public object 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..." })%>
输出
<input class="styledInput" id="name" name="name"
size="50" type="text" value="Please enter your name..." />
与 Jeff 的代码相比,字符可能多了一些,但计划是让编码更容易。我想知道哪种方法最好,只有尝试一下才能知道。
摘要
为了说明这一切,源代码展示了如何创建一个发送短信的应用程序。默认视图包含捕获所有必需信息的表单,这些信息会传递到 HomeController
的 SendMessage()
方法,在该方法中进行基本验证。有空间可以将其链接到我们的一个 SDK/API,这意味着消息将被发送——注册一个 免费试用。
我还没有完成这个库,但这是一个很好的起点。希望在阅读了这篇文章之后,您会认识到今天花一点额外时间来产生一些将来能为您节省时间的东西的好处。
我们的目标是在 2009 年第一季度发布 Doyle 进行公开测试。按计划,所有视图都使用 MvcControls 库,我们对结果非常满意。这是我们撰写页面的一览。
* 要运行演示,您需要安装 Microsoft .NET Framework 3.5 和 Microsoft ASP.NET MVC Beta。
致谢
一些代码片段是从 Microsoft ASP.NET MVC Beta 源代码复制的。特别是,UrlHelperExtensions
只是公开了已经用 internal
关键字定义的那些方法。
我想再次感谢 Jeff Handley 的原创想法。