实用 ASP.NET MVC (3) 技巧






4.97/5 (129投票s)
一系列技巧,涉及 Entity Framework、扩展方法、编程模式等,这些技巧是在最近的 ASP.NET MVC 3 编程实践中积累起来的。
目录
- 引言
- 背景
- 技巧 1:分离实体和模型
- 技巧 2:重新验证更新的内容
- 技巧 3:保护您的网站安全
- 技巧 4:始终使用 DAL
- 技巧 5:设置正确的 IoC 依赖项解析器
- 技巧 6:在 MVC 3 中使用 MVC 4 的打包功能
- 技巧 7:使用 MySQL 作为数据库(配合 Membership)
- 技巧 8:检测 jQuery AJAX 请求
- 技巧 9:预编译视图以最大限度地减少错误
- 技巧 10:使用 TagBuilder
- 技巧 11:包含非常有用的扩展方法
- 技巧 12:您绝不能忘记的特性
- 技巧 13:小心处理外键
- 技巧 14:本地化操作 - 机遇与陷阱
- 技巧 15:使用约束以获得更好的路由
- 技巧 16:使用操作参数名称时要小心
- 技巧 17:向视图添加命名空间
- 技巧 18:内部操作
- 技巧 19:通过缓存提升性能
- 技巧 20:重写控制器的方法
- 技巧 21:自定义 Membership Provider
- 技巧 22:如何强制使用 HTTPS
- 技巧 23:使用 T4MVC 进行强类型辅助
- 使用代码
- 关注点
引言
从我第一次看到 ASP.NET MVC 的那一刻起,我就知道它不仅有用,而且功能强大。然而,能力越大,责任越大(在技术领域:要求也越高),这导致了陡峭的学习曲线。本文不关注专业的 ASP.NET MVC 开发人员(我猜他们确实知道我在本文中将要写的一切),而是献给那些刚刚开始或计划进行 ASP.NET MVC (3) 开发的人。
大多数技巧和源代码将侧重于 MVC 核心,而其他则侧重于可以组合使用的技术,例如 Entity Framework 或 jQuery 的验证助手。本文还将包含更专业的主题,例如使用 Unity 依赖项解析器的 IoC,或者使用 MySQL 数据库而不是 Microsoft SQL 数据库。尽管有些技巧可能对某些人不相关,而另一些技巧可能被其他人知道,但我认为它们都值得写下来。
本文无意教您 MVC、HTML、JavaScript 或 CSS。在本文中,我将为您提供一系列(不连贯的)技巧,这些技巧在处理 ASP.NET MVC 时可能很有帮助。其中一些技巧可能会随着时间的推移而过时,然而,每个技巧都将包含一个教训(或者对我来说,在我被困住的时候,它就包含了一个教训!)。
背景
ASP.NET MVC 可能是构建动态网页的最佳方法。现在这是一个相当强烈的说法,会有人强烈反对这个观点。是什么让 ASP.NET MVC 如此出色?一方面,您可以使用 C#(或 VB.NET - 但我强烈推荐 C#)来编写它。C# 是一种静态类型语言,最初是 Java 的克隆,但如今包含了更多最先进的功能。即使语言是静态的,您仍然可以访问反射、动态变量和匿名对象等强大功能。甚至匿名方法,即所谓的 lambda 表达式,也是可能的。总而言之,该语言运行速度非常快(对于托管语言而言)并且是 JIT 编译的。尽管无法达到 C++ 的性能水平,但您可以轻松地获得比任何脚本语言更好的性能。
现在有人可能会争辩说性能不是一切,或者如果是,那么您总是可以交叉编译到二进制文件。这里是 ASP.NET MVC 的第二个论点:由于它建立在 .NET / Visual Studio 堆栈之上,因此您可以访问一个非常好的调试器和非常好的工具来完成工作。多年来,这也有点反驳了,因为 ASP.NET(现在没有 MVC)对大多数人来说是 WebForms 的同义词。WebForms 堆栈非常强大且深入,但它的缺点是消耗性能并让您(程序员)失去控制。您只需点击一下,编程一点等等,但最终结果(即标记)与您的输入截然不同。找出后端发生了什么也是一项艰巨的任务。
这就是 ASP.NET MVC 的用武之地。MVC 概念相当古老,对于构建图形用户界面非常有用。如今,MVC 模式已在 Ruby on Rails 的 Web 应用程序构建中获得广泛认可。由于 Ruby 是一种具有非 C 样语法动态语言,我们确实需要 ASP.NET MVC 作为对应的。那么我们最终得到的是什么?
- 完全控制我们的输出
- 面向对象设计
- 建立在强大的 ASP.NET 核心堆栈之上
- 使用 Razor 视图引擎的非常现代和优雅的视图编写风格
- 访问其他 .NET 库和 C/C++ 库
- (JIT)编译的源代码以提高性能
- 出色的调试支持
- 一种具有动态类型和其他现代功能的静态语言
如果您还没有尝试过 ASP.NET MVC,但您了解 C# 或 .NET Framework(甚至 ASP.NET),那么您应该立即尝试一下!如果您已经这样做了,现在知道发生了什么,并且想了解更多信息,那么本文是您的不二之选。
技巧 1:分离实体和模型
这个第一个技巧一开始可能看起来微不足道(或不是),但我花了很多时间才理解它。在这种情况下,当我们谈论实体时,我们指的是数据库表中的一行。大多数初学者/中级 MVC 用户会犯的错误是混淆了模型和实体。这两者应该始终分开,每个视图应该只接收最少需要的数据。让我们考虑以下视图的情况
@model TestModel
<p>Hallo @Model.Name!</p>
<p>You are back online!</p>
这个(强类型的)视图看起来很简单。这里我们只需要给定 `TestModel` 实例的 `Name` 属性。所以 `TestModel` 可能看起来像
public class TestModel
{
public string Name { get; set; }
}
也许 `TestModel` 有更多属性,但在这个例子中没有设置。这无关紧要,只要 `TestModel` 不是数据库实体,例如
public class TestEntity
{
public int Id { get; set; }
public string Name { get; set; }
public string FirstName { get; set; }
public string Street { get; set; }
public string City { get; set; }
public DateTime Birthday { get; set}
}
在这种情况下,进行一个查询来请求(并填充)所有这些属性似乎工作量太大了。但即使性能问题可能是一个很好的论点,还有一个更好的论点是只使用实际模型来填充视图。考虑一下,如果我们在视图中需要另一个属性,但这个属性不在实体 `TestEntity` 中。它可能与主键 `Id` 相关。有趣的效果是,我们需要更改参数,因为我们无法扩展实体(这个实体与表结构相关 - 表结构当然会保持原样)。所以这里的问题是可扩展性。通过为每个视图使用特定的(或更通用的)模型,我们获得了可扩展性,并可以减少实体框架的工作量。
有关模型/实体对象讨论的更多信息,请参阅 Stackoverflow。
技巧 2:重新验证更新的内容
MVC 的一个很酷的功能是验证。网页设计师有一个梦想:在服务器端和客户端进行验证。如果有人拥有丰富的 HTML5 功能(和/或启用了 JavaScript),则会在客户端无缝执行验证,无需任何页面请求。这会减少流量,加快页面执行速度,并带来更多好处。然而,有些用户可能没有这些功能,或者会禁用它们来寻找服务器端验证中的潜在漏洞。因此,服务器上的验证规则必须始终与客户端上的至少相同。使用 ASP.NET MVC,每个用户都会获得一个可用的 jQuery 副本和一些强大的插件。其中一个插件负责验证,而另一个插件负责以不显眼的方式执行前一个插件。这意味着插件会扫描加载的文档以查找特殊标签和属性。在此搜索之后,将为适当的元素提供所需的事件处理程序。
如果我们想要一个页面加载和卸载,这个过程效果很好,但如果我们要更新一部分内容,效果就不那么好了。更新可以通过 AJAX 请求或我们自己的 JavaScript 修改来完成。问题是我们需要在页面加载后为某个区域(更新的区域)设置验证。在这种情况下,我们需要调用验证脚本的一个方法。然后,此方法会将实时验证挂接到选定的元素上。以下代码片段说明了用法
$.validator.unobtrusive.parse('#Content');
这里,ID 为 Content 的元素将被解析并设置实时验证。更多信息可以在 Stackoverflow 上找到。
技巧 3:保护您的网站安全
安全始终是一个大问题。当然,当源代码是公开的时,更容易发现漏洞。由于网站就是这种情况,因此我们需要格外小心我们从服务器接收到的数据。可能的漏洞可能来自恶意用户或被劫持的用户。后一种情况可能是由于跨站点脚本(XSS)、病毒等恶意程序或用户自身软件的问题。无论哪种情况,我们都需要做好准备。大多数 XSS 情况实际上可以通过使用 AntiForgeryToken()
方法来预防。为了在网站上使用它,我们只需在表单中插入一个语句
@using(Html.BeginForm())
{
@Html.AntiForgeryToken()
<!-- Rest Of Our Form -->
}
此语句将创建一个隐藏的表单元素,其中包含一个仅在服务器上(临时变量)知道的随机序列。当用户提交表单时,我们仍然需要验证令牌。可以通过一个特性(ValidateAntiForgeryToken
特性)来完成验证。让我们看看如何保护 `Test` 控制器的 `ProtectMe` 操作
public class TestController : Controller
{
/* ... */
//
// POST: /Test/ProtectMe
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult ProtectMe(ProtectMeModel data)
{
if(ModelState.IsValid)
{
/* What to do if valid? */
return RedirectToAction("Protected");
}
/* What to do if invalid ? */
return View(data);
}
}
在这里调用 `Model.IsValid` 属性至关重要,因为该特性会通过添加模型错误语句来指示使用了错误的防伪令牌。因此,即使提交的数据对于此模型来说似乎有效,提交也将无效。
跨站点脚本可能是最大的威胁,所以我们不仅要阻止任何被劫持的请求,还要阻止我们的服务器响应包含恶意脚本和请求。如果用户生成的内容根本不允许包含任何 HTML,我们基本上就是安全的,但这并不总是可能的。如果允许用户输入 HTML,我们有两种处理方式
- 将所有具有潜在恶意风险的标签和属性列入黑名单
- 将所有必需且不可能包含恶意内容的标签列入白名单
第一种方法对大多数用户来说似乎很有吸引力(因为他们没有考虑到所有可能性)。我们始终偏爱第二种方法,因为它通常不需要授予用户过多的标签(他们无论如何都需要做什么?),而且由于列表可以更容易地控制(例如,考虑到随着 HTML 标准的未来规范,标签的数量不断增加)。
不幸的是,MVC 3 并没有开箱即用地包含一个清理器库。幸运的是,其他人付出了巨大的努力来编写一个,从而产生了 AntiXSS 库。要使用标准(安全)标签列表转换(HTML)输入,我们只需要调用 `Sanatizer.GetSafeHtmlFragment()`。强烈建议使用此类库,因为它们通常会定期更新,从而实现最先进的清理器。该库可以在 Microsoft 下载。
这个部分要说的最后一点是适当的授权。授权非常重要,应该被视为每个网站的强制性元素。我们所有的操作在代码中都是公开的,因此可以通过 HTTP 请求访问。这意味着任何用户,无论好坏,都可以访问这些操作。一个好的起点是使用 `[Authorize]` 特性。它可以用于控制器和操作。如果我们将其用于控制器,例如
[Authorize]
public class TestController : Controller
{
/* ... */
}
这将保护任何内部操作免遭未登录用户的访问。如果我们希望未经授权的用户访问任何操作,我们需要将 `[Authorize]` 特性放在其他操作之上,例如
public class TestController : Controller
{
/* ... */
[Authorize]
public ActionResult ShouldBeProtected()
{
/* ... */
}
[Authorize]
public ActionResult AlsoOnlyForLoggedInUsers()
{
/* ... */
}
public ActionResult AccessibleByEveryone()
{
/* ... */
}
}
我们可能甚至需要区分这些授权用户。基本上,我们可以通过使用他们的用户名或(更通用地)他们的角色来实现。每个用户都有一个分配的用户名,加上零个或多个角色。一个非常重要的角色可能是Administrators。如果我们希望某个操作只能由属于Administrators角色的用户访问,我们需要具有以下特性
public class TestController : Controller
{
/* ... */
[Authorize(Role = "Administrators")]
public ActionResult ShouldBeProtected()
{
/* ... */
}
}
可以输入更多角色或混合角色和用户名。但是,通常使用固定用户名是一种不灵活的编码技术,迟早会导致问题。
Tomas Jansson 走得更远,他将我们 Web 应用程序的安全性方向颠倒了。因此,一开始一切都已锁定,我们必须明确地解锁访问(对特定组或所有用户)。这无疑是防止意外留下开放大门的好方法。Tomas 的文章题为 Securing your ASP.NET MVC3 application。
技巧 4:始终使用 DAL
数据访问层是提供对某种持久性存储(例如实体关系数据库)中存储的数据的简化访问的层。这个定义已经告诉我们,DAL 基本上是我们 Web 应用程序和数据库之间的一个抽象。通过使用 Entity Framework,我们对数据库的访问已经得到了简化。这种方法使我们能够编写(经过编译器检查的)LINQ 查询。另一个优点是获得 C# 对象形式的数据,而不是数据库中的纯字符串/对象。
总而言之,直接使用数据库就像直接在炉灶上烧水,而不是在中间使用炊具作为一层。后者提供了几个优点,例如更干净、更健壮地实现同一目标。现在,为了拥有一个真正的 DAL,我们需要的不只是 Entity Framework。实际上,Entity Framework 可能会更像上面比喻中的电磁炉(而火就是直接使用 SQL)。那么我们的炊具在现实世界中(或者说,在我们虚拟的编程世界中)是什么样的呢?
- 需要访问 Entity Framework,即数据库实体。
- 只提供(已抽象的)方法,即不提供完整的表或直接查询,而是预先准备好的查询。
- 返回模型而不是实体。
我们可以使用所谓的存储库模式来抽象 Entity Framework。Scott Gu 在 Nerddinner Book Pt 3 中提供了一个非常基本的示例。但首先,让我们看看如何使用 Entity Framework 设置我们的数据库
public class MyDatabase : DbContext
{
public DbSet<EntityType> TableName { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<IncludeMetadataConvention>();
}
}
我们只需要创建一个继承自 `DbContext` 的类。新类的名称必须等于 web.config 中连接的名称(稍后会有更多介绍)。这里我们只有一个 `EntityType` 类型的表。表名将是 `TableName`。通常的约定是表名是实体类名的复数。
另一个特性是元数据约定。如果数据库表不可用且用户权限足够,实体框架还可以构造数据库。为了记住上次数据库构建期间的状态,会设置一个特殊表。该表将包含元数据。由于这是用于测试/非生产系统的,我们通常希望删除此约定。
通过定义类来设置我们的数据库连接,并将它们组合在 `MyDatabase` 类中,我们就可以构建我们的实际 DAL 了。
public class DatabaseRepository
{
// The database is a member variable
MyDatabase db;
// List methods
public SomeSpecificModel ListAllEntities()
{
/* ... */
}
// Insert methods
public void AddNewEntity(AnotherSpecificModel data)
{
/* ... */
}
// Save method - just one example
public void Save()
{
db.SaveChanges();
}
}
现在我们正在使用具有 `DatabaseRepository` 类的正确 DAL,我们还可以使用更复杂的方法来构造它。一种方法是将 `DatabaseRepository` 制成一个接口,称为 `IRepository`。下一个技巧将处理基于此类实现规则的 IoC 模式。
技巧 5:设置正确的 IoC 依赖项解析器
控制反转 (IoC) 模式是 ASP.NET MVC 中最常用的设计模式之一。它的主要优点是可以在不了解所有依赖项的情况下构造对象。为了将这种编程风格用于我们自己的控制器和其他内容,我们需要设置一个正确的依赖项解析器。
已经有一些经过良好测试且效果不错的依赖项解析器解决方案。其中最常用的一种称为 Unity。我们所需要做的就是告诉服务定位器,哪个依赖项可以通过传递哪个对象来解析。因此,我们可以构建一个类,如下所示(`RepositoryResolver`)
public class RepositoryResolver : IDependencyResolver
{
readonly IUnityContainer _container;
public RepositoryResolver(IUnityContainer container)
{
this._container = container;
}
public object GetService(Type serviceType)
{
try
{
return _container.Resolve(serviceType);
}
catch
{
return null;
}
}
public IEnumerable<object> GetServices(Type serviceType)
{
try
{
return _container.ResolveAll(serviceType);
}
catch
{
return new List<object>();
}
}
}
接下来,我们将类集成到解析依赖项中。因此,我们在 global.asax.cs 文件中实现以下代码。
public class MvcApplication : HttpApplication
{
/* ... */
public static void RegisterDependencies()
{
var container = new UnityContainer();
container.RegisterType<IDependency, ImplementionOfIDependency>();
DependencyResolver.SetResolver(new RepositoryResolver(container));
}
protected void Application_Start()
{
/* ... */
RegisterDependencies();
}
}
这将自动构造如下控制器
public class DependentController : Controller
{
public DependentController(IDependency someDependency)
{
/* Do Something With Dependency */
}
}
所以,要让 ASP.NET MVC 中的依赖项解析器完全正常工作,我们只需要实现 `IDependencyResolver` 并通过调用 `SetResolver()` 方法将解析器(或其中一个)添加到可用的 `DependencyResolver` 对象中。
有关更多信息,请访问 Brad Wilson 的博客。
技巧 6:在 MVC 3 中使用 MVC 4 的打包功能
当然,ASP.NET MVC 4 提供了比 MVC 3 更多的功能。现在的问题是:我们真的需要所有这些功能吗?当然,今天我们仍然可以用 ASP.NET MVC (1) 来工作,但有些功能就是太方便了,以至于难以错过这两次迭代。现在,假设我们无法升级到 MVC 4,那么在 MVC 3 中,对于通用的 Web 应用程序开发,还有哪些功能仍然很受欢迎?在我看来,就是 MVC 4 的打包功能,我们在 MVC 3 中不应该错过它。使用 Nuget 可以很容易地实现。如果您还没有检查过 Nuget,您可能应该阅读一篇或几篇相关的文章(无论是这里 CodeProject 还是官方网站)。Nuget 是一个非常好的数据包管理器,它提供了许多出色的功能。Nuget 的任务之一就是检查已安装程序包的更新。这有助于我们跟踪我们的第三方库(甚至是我们自己的库跨越多个系统)。
在完全安装 Nuget 程序包管理器后,我们只需在 Nuget PowerShell 控制台中键入以下命令
Install-Package Microsoft.Web.Optimization -Pre
这将把 ASP.NET MVC 4 的打包功能安装到解决方案中。安装意味着将引用二进制文件,所有依赖项也将被解析和安装。现在我们就可以使用打包管理器的预览版本了。打包过程将获取所有找到的或给定的 JavaScript 或 CSS 文件,并将它们合并成一个。这将导致更少的页面请求,为我们的用户带来更快的页面加载,为我们带来更少的服务器负载。此外,打包类还提供最小化功能,即它甚至会减小 JavaScript 和 CSS 文件的大小。打包管理器是可扩展的,这意味着我们也可以为自己的文件类型编写支持,或将其用作转译器(例如 CoffeeScript 到 JavaScript 等)。要启用打包管理器,我们需要在 global.asax.cs 文件中进行设置
public class MvcApplication : HttpApplication
{
/* ... */
protected void Application_Start()
{
BundleTable.Bundles.EnableDefaultBundles();
/* ... */
}
}
让我们首先直接从视图中使用它
<p>Here is some view content!</p>
<p>Some more content and now our JavaScripts ...</p>
<script src="@System.Web.Optimization.BundleTable.Bundles.ResolveBundleUrl("~/Scripts/js")"></script>
这里我们只指定一个目录和一个文件扩展名(在本例中是 JavaScript)。然后,打包管理器将搜索目录中的所有 *.js 文件,将它们合并、最小化,并响应有关此 URL 的任何请求。在大多数情况下,打包管理器非常智能,它会将 jQuery 等文件放在可能的 jQuery 插件之上,并排除 vsdoc 文件(没有 vsdoc 名称的 JavaScript 文件的文档)。然而,有时我们只想要特定目录中特定顺序的特定文件。在这种情况下,我们可以创建自己的包。最好的方法是(再次)在 global.asax.cs 文件中。这里我们只需插入以下代码
public class MvcApplication : HttpApplication
{
/* ... */
void RegisterBundles(BundleCollection bundles)
{
/* Now we create a specialized jQuery bundle */
var jQueryBundle = new Bundle("~/Scripts/jquery", new JsMinify());
jQueryBundle.AddDirectory("~/Scripts", "jquery*.js",
searchSubdirectories: false, throwIfNotExist: true);
bundles.Add(jQueryBundle);
/* Now we create a very special bundle */
var specialBundle = new Bundle("~/Scripts/special", new ScopeMinify());
specialBundle.AddFile("~/Scripts/game/main.js");
specialBundle.AddFile("~/Scripts/editor/scroll.js");
specialBundle.AddFile("~/Scripts/editor/editor.js");
specialBundle.AddFile("~/Scripts/site/ready.js");
bundles.Add(specialBundle);
}
protected void Application_Start()
{
RegisterBundles(BundleTable.Bundles);
/* ... */
}
}
ScopeMinify
是我们自己的类,看起来像
public class ScopeMinify : JsMinify
{
public override void Process(BundleContext context, BundleResponse response)
{
response.Content = "(function() {" + response.Content + "})();";
#if !DEBUG
base.Process(context, response);
#endif
}
}
因此,此类使用立即执行的匿名方法(也称为 IIFE(立即调用函数表达式)模式)将所有包含的 JavaScript 文件范围化。如果我们处于生产环境,JavaScript 也会被最小化(调试起来更难,因为对象会被重命名),否则它只会打包和范围化。
设置好打包包后,我们只需要调用相应的 URL。让我们再次从视图中调用它
<script src="@System.Web.Optimization.BundleTable.Bundles.ResolveBundleUrl("~/Scripts/special")"></script>
打包是一种获得更高性能并稍微提高 Web 应用程序安全性的好方法,因为我们可以使用匿名方法来限定生产 JavaScript。它还将阻止大多数用户尝试读取我们的 JavaScript 源代码(当然,最邪恶的鱼仍然在水中)。
有关更多信息,请参阅 Joey Iodice 的博客。Jef Claes 在他的文章 ASP.NET MVC 4 Bundling in ASP.NET MVC 3 中也写了关于打包功能的内容。
技巧 7:使用 MySQL 作为数据库(配合 Membership)
有时人们会抱怨 .NET 或 ASP.NET (MVC) 只能使用 MSSQL 或 SQL Express(或其他 Microsoft 产品)作为其数据库系统。这完全不正确,因为 ASP.NET MVC 构建在 ASP.NET 之上,而 .NET 这个名字是有原因的。 .NET 的核心组件之一称为 ADO.NET,它原则上允许程序员使用任何数据库系统。我们只需要一个连接器。在 MySQL 的情况下,我们可以选择几种可用的连接器。官方且被广泛接受的连接器称为 MySQL .NET Connector。
使用官方程序包可以为我们带来最完整的功能。在本技巧中,我们将研究在 web.config 文件中需要下载和设置哪些内容才能访问具有完整功能的 MySQL 数据库以及标准的 ASP.NET Membership Provider(编写自己的 Membership Provider 始终是可能的 - 请参阅技巧 21)。首先,我们从 mysql.com/downloads/connector/net/ 下载 MySQL Connector/NET。安装过程(也应该在我们的服务器上完成)完成后,我们需要更改 web.config 文件中的几行
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<connectionStrings>
<add name="DatabaseConnection"
connectionString="Server=localhost;Port=3306;Database=yourdbname;Uid=yourusername;Pwd=yourpassword;"
providerName="MySql.Data.MySqlClient" />
</connectionStrings>
<!-- ... -->
</configuration>
这将启用 `DatabaseConnection` 作为我们的主数据库连接。根据约定(对于 Entity Framework),我们需要将继承自 `DbContext` 的类的名称命名为 `DatabaseConnection`。现在 Entity Framework 将与 MySQL 一起作为数据库工作。那么 Membership Provider 呢?嗯,在这种情况下,我们需要进一步扩展 web.config 文件。让我们看看
<configuration>
<!-- ... -->
<system.web>
<!-- ... -->
<authentication mode="Forms">
<forms loginUrl="~/Account/LogOn" timeout="2880" />
</authentication>
<roleManager enabled="true" defaultProvider="MySQLRoleProvider">
<providers>
<clear/>
<add name="MySQLRoleProvider" autogenerateschema="true"
type="MySql.Web.Security.MySQLRoleProvider, MySql.Web, Version=6.5.4.0,
Culture=neutral, PublicKeyToken=c5687fc88969c44d"
connectionStringName="MarioDB" applicationName="/" />
</providers>
</roleManager>
<membership defaultProvider="MySQLMembershipProvider">
<providers>
<clear />
<add name="MySQLMembershipProvider" autogenerateschema="true"
type="MySql.Web.Security.MySQLMembershipProvider, MySql.Web,
Version=6.5.4.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d"
connectionStringName="MarioDB" enablePasswordRetrieval="false"
enablePasswordReset="true" requiresQuestionAndAnswer="false"
requiresUniqueEmail="true" maxInvalidPasswordAttempts="5"
minRequiredPasswordLength="6" minRequiredNonalphanumericCharacters="0"
passwordAttemptWindow="10" applicationName="/" />
</providers>
</membership>
<!-- ... --->
</system.web>
</configuration>
现在这里有一个重要的属性被设置了。如果我们没有将 `autogenerateschema` 设置为 `true`,那么我们需要准备好相应的表供 Membership 和 Role Provider 使用。这是不必要的工作,这意味着我们应该始终先允许生成表。完成后为什么还要删除它?一方面,数据库用户不应该拥有过多的权限,因此一旦完成工作,就应该删除表创建权限。另一方面,额外的检查表是否存在的检查是不必要的,为我们节省了一点时间。
技巧 8:检测 jQuery AJAX 请求
有时我们想通过 AJAX 请求获取资源。这项技术通过限制输出并提供更好的用户体验来节省资源。即使我们执行 AJAX 请求,我们仍然需要包含使用旧浏览器或禁用 JavaScript 引擎的用户。在本技巧中,我们将省略客户端的实际实现,而专注于服务器端。场景如下:请求一个页面,无论请求是由 jQuery 还是直接发出的,我们都发送一个部分结果,其中包含应更新的部分,或者发送一个完整页面。
幸运的是,jQuery 的作者在这方面非常聪明,并为每个 AJAX 请求添加了一个特定的头名称和值。ASP.NET MVC 的创建者意识到了这一点,并包含了一个扩展方法来指示我们是否使用了 AJAX。扩展方法 `IsAjaxRequest()` 位于任何控制器都可以访问的 `Request` 对象中。
因此,我们可以写一个如下的控制器操作
public ActionResult UploadImage()
{
/* Do general work */
//Just to distinguish between an AJAX request (for: modal dialog) and normal request
if (Request.IsAjaxRequest())
return PartialView(data);
return View(data);
}
在 AJAX 的情况下,我们将仅更新网页的内容区域。在任何其他情况下,我们将通过响应完整内容来更新整个页面。
技巧 9:预编译视图以最大限度地减少错误
一旦我们完成了 MVC 应用程序的编写,我们可能会以发布模式发布代码。我们可能已经写了很多代码,生成了许多视图,甚至创建了很多测试。测试都很好,一切似乎都到位了,然而,唯一无法测试的是视图。如果任何视图包含错误,我们只有在访问它时才会意识到。这是一个大问题——但可以避免。通过编译视图,我们可以进行基本检查,看看我们是否遗漏了什么。
要进行编译设置,我们需要更改项目的 XML 文件。以下是操作方法
<MvcBuildViews>false</MvcBuildViews>
改为
<MvcBuildViews>true</MvcBuildViews>
- 在解决方案资源管理器中右键单击项目
- 点击“卸载项目”
- 现在再次右键单击项目
- 这次点击“编辑”
- 更改以下行
- 保存文件并关闭它
- 再次右键单击项目
- 点击“重新加载项目”
就是这样!现在我们的视图将像其他源代码一样进行编译。请注意,这可能无法满足所有人。通常,为发布版本激活编译选项就足够了。
有关该主题的更多信息,请参阅 Stackoverflow 问题。David Ebbo 还在 MSDN 上发表了 Turn your Razor helpers into reusable libraries 这篇文章,这与之密切相关。在 Tugberk Ugurlu 的博客 上提供了设置编译的完整指南。
技巧 10:使用 TagBuilder
构造完全有效的(HTML)标签非常容易。我们不必纠缠于 HTML(可能会包含错别字,例如缺少引号)来生成 HTML 输出。大多数人首先认识到的是,每个字符串都会被 HTML 编码。让我们考虑以下视图
@{
var title = "<span class=mark>HI, Mum!</span>";
}
<p>@title</p>
服务器的(部分)响应不是预期的
<p><span class=mark>HI, Mum!</span></p>
而是以下内容
<p><span class=mark>HI, Mum!</span></p>
这实际上是 MVC(或 Razor 视图引擎)的一个特性。如果我们想要 HTML,我们必须明确告诉系统。有两种方法可以做到这一点
- 我们使用 HTML 帮助程序的 `Raw()` 扩展方法,它接受一个字符串并按原样输出。
- 我们不输出字符串值,而只输出实现 `IHtmlString` 的 `MvcHtmlString` 或类似实例。
由于我们不能总是选择方案一,我们将不得不处理方案二。幸运的是,构造 `MvcHtmlString` 非常容易,因为构造函数只需要一个字符串作为参数,或者因为我们可以只使用 `MvcHtmlString` 类的静态 `Create()` 方法。
现在,HTML 字符串的构造过程可以使用许多字符串连接或 `StringBuilder` 实例开始。然而,最好的方法是使用 `TagBuilder`。创建它所需要做的就是告诉构造函数标签的名称。然后,我们可以根据需要添加或删除自定义属性。我们也可以添加或删除 CSS 类。另一个特性是设置有效的 ID。由于某些符号是禁止的,`TagBuilder` 将自动清理 ID 字符串,使其有效。最后,我们还可以设置内部文本或 HTML。
现在我们考虑构建自己的图片链接的情况。我们可以这样做
public static MvcHtmlString ImageLink(string link, string src)
{
var tag = new TagBuilder("a");
tag.Attributes.Add("href", link);
var img = new TagBuilder("img");
img.Attributes.Add("src", src);
img.Attributes.Add("alt", string.Empty);
tag.InnerHtml = img.ToString(TagRenderMode.SelfClosing);
return MvcHtmlString.Create(tag.ToString(TagRenderMode.Normal));
}
在这里,我们使用了两个 `TagBuilder`(每个标签一个)实例。此类的一个特性是重载的 `ToString()` 方法。在这里,我们可以设置最终的外观。由于图像标签是自闭合的,我们选择它作为图像标签。然后,图像标签被设置为锚点标签的内部 HTML。最后一步是创建最终的 `MvcHtmlString`。
技巧 11:包含非常有用的扩展方法
ASP.NET MVC 大量使用了 C# / VB.NET 的新特性。最常使用的特性之一是扩展方法。理论上,您可以重写所有可用的扩展方法(例如,视图中 `Html` 对象的一些方法),并通过替换适当的命名空间来用您自己的版本替换所有现有调用。这意味着我们可以通过更改使用的命名空间来轻松地用我们自己的方法替换现有方法。
大多数时候,我们对现有的扩展方法感到满意,但我们想要一些额外的。一个好习惯是创建一个目录,其中包含我们要用自己方法扩展的每个类的文件。我们可以将该子目录命名为Extensions,并在其中添加静态类,如 `HtmlExtensions`、`UrlExtensions` 等。一些更标准的(未在开箱即用的情况下提供的)扩展方法是
- 一个用于提交文件的 `
- 文件输入元素
- 一个操作链接,它围绕一个 HTML 图像标签而不是简单的文本
- 一些 HTML5 表单输入元素,例如 `` 元素
提供这些扩展方法的一种可能方式如下
public static class HtmlExtensions
{
public static MvcForm BeginFileForm(this HtmlHelper html)
{
return html.BeginForm(null, null, FormMethod.Post, new { enctype = "multipart/form-data" });
}
public static MvcHtmlString File(this HtmlHelper html, string name, bool multiple)
{
var tb = new TagBuilder("input");
tb.Attributes.Add("type", "file");
tb.Attributes.Add("name", name);
tb.GenerateId(name);
if (multiple)
tb.Attributes.Add("multiple", "multiple");
return MvcHtmlString.Create(tb.ToString(TagRenderMode.SelfClosing));
}
public static MvcHtmlString File(this HtmlHelper html, string name)
{
return html.File(name, false);
}
public static MvcHtmlString MultipleFileFor<TModel, TProperty>(this HtmlHelper<TModel> html,
Expression<Func<TModel, TProperty>> expression)
{
string name = GetFullPropertyName(expression);
return html.File(name, true);
}
public static MvcHtmlString FileFor<TModel, TProperty>(this HtmlHelper<TModel> html,
Expression<Func<TModel, TProperty>> expression)
{
string name = GetFullPropertyName(expression);
return html.File(name);
}
public static MvcHtmlString ActionImage(this HtmlHelper html, string imageUrl,
string action, string controller, object routeValues, object htmlAttributes)
{
var urlHelper = new UrlHelper(html.ViewContext.RequestContext);
var link = new TagBuilder("a");
link.Attributes.Add("href", urlHelper.Action(action, controller, routeValues));
var img = new TagBuilder("img");
img.Attributes.Add("src", imageUrl);
img.Attributes.Add("alt", action);
link.InnerHtml = img.ToString(TagRenderMode.SelfClosing);
return MvcHtmlString.Create(link.ToString(TagRenderMode.Normal));
}
public static MvcHtmlString Email(this HtmlHelper html, string name)
{
return html.Email(name, string.Empty);
}
public static MvcHtmlString Email(this HtmlHelper html, string name, string value)
{
var tb = new TagBuilder("input");
tb.Attributes.Add("type", "email");
tb.Attributes.Add("name", name);
tb.Attributes.Add("value", value);
tb.GenerateId(name);
return MvcHtmlString.Create(tb.ToString(TagRenderMode.SelfClosing));
}
public static MvcHtmlString EmailFor<TModel, TProperty>(this HtmlHelper<TModel> html,
Expression<Func<TModel, TProperty>> expression)
{
var name = GetFullPropertyName(expression);
var value = string.Empty;
if(html.ViewContext.ViewData.Model != null)
value = expression.Compile()((TModel)html.ViewContext.ViewData.Model).ToString();
return html.Email(name, value);
}
static string GetFullPropertyName<T, TProperty>(Expression<Func<T, TProperty>> exp)
{
MemberExpression memberExp;
if (!TryFindMemberExpression(exp.Body, out memberExp))
return string.Empty;
var memberNames = new Stack<string>();
do
{
memberNames.Push(memberExp.Member.Name);
}
while (TryFindMemberExpression(memberExp.Expression, out memberExp));
return string.Join(".", memberNames.ToArray());
}
static bool TryFindMemberExpression(Expression exp, out MemberExpression memberExp)
{
memberExp = exp as MemberExpression;
if (memberExp != null)
return true;
if (IsConversion(exp) && exp is UnaryExpression)
{
memberExp = ((UnaryExpression)exp).Operand as MemberExpression;
if (memberExp != null)
return true;
}
return false;
}
static bool IsConversion(Expression exp)
{
return (exp.NodeType == ExpressionType.Convert || exp.NodeType == ExpressionType.ConvertChecked);
}
}
`ActionImage()` 方法使用 `UrlHelper` 实例的创建来生成相应的超链接。这可以通过使用当前 `HtmlHelper` 对象的 `ViewContext.RequestContext` 属性来完成。
有时非常有用的扩展是以扩展方法的形式提供的。让我们看看一个非常有用的扩展,称为 Captcha MVC。该扩展可通过 Nuget 或 官方 CodePlex 页面 获得。此扩展为我们提供了包含验证码(一种验证用户确实是人类而不是计算机程序的方法)的可能性,以防止自动使用我们的网页。这将减少非人类用户的表单提交。为了使用该扩展,我们只需执行以下步骤
- 下载 Captcha MVC
- 将扩展安装到我们的解决方案中
- 在 web.config 文件中设置加密密钥
- 为视图包含扩展方法的命名空间
- 在表单中插入 `@Html.Captcha("YourText", 5)` 指令,我们想在哪里放置验证码
- 在操作之上插入 `CaptchaVerify` 特性,我们想在哪里检查验证码
Captcha MVC 的扩展方法应稍作修改,以便为我们的网页提供我们需要的一切。标准实现如下
public static class HtmlExtensions
{
public static MvcHtmlString Captcha(this HtmlHelper htmlHelper, string textRefreshButton, int length)
{
return CaptchaHelper.GenerateFullCaptcha(htmlHelper, textRefreshButton, length);
}
public static MvcHtmlString Captcha(this HtmlHelper htmlHelper, int length)
{
return CaptchaHelper.GenerateFullCaptcha(htmlHelper, length);
}
}
最后,我们还需要设置一些 CSS 样式来调整验证码的外观和感觉,使其与我们的 Web 应用程序一致。
这张图片展示了使用验证码扩展的最终网站可能是什么样子。这里我们使用了带有德语文本的标准代码。唯一的区别在于 CSS 规则,这些规则无论如何都需要根据我们的特定页面进行调整。
技巧 12:您绝不能忘记的特性
正如我们已经看到的,特性是 ASP.NET MVC 的一个非常重要的特性(或者更准确地说:是 C# 的一个特性,在 ASP.NET MVC 中得到了广泛使用)。然而,控制器并不是唯一需要正确特性的地方。通过 MVC 构建的数据模型由给定的特性进行验证。如果没有指定特性,MVC 将假定一些默认值。这可能会导致不希望产生的副作用。一个非常流行的误解是,字符串的长度可以达到 .NET Framework 中字符串长度的最大值。然而,事实并非如此。字符串长度的默认值为 128。这有时会导致混淆,正如在 StackOverflow 页面上所讨论的。
要超过此默认字符串长度,我们需要指定最大长度。如果我们想使用可能的最大大小,我们只需要 `MaxLength` 特性。其他选项是使用 `StringLength` 特性。考虑到字符串,我们还需要考虑(任何)HTML 输入。如果检测到 HTML 输入,ASP.NET 运行时通常会抛出错误。通过告诉 MVC 某个参数允许向我们的服务器提交 HTML 值,可以避免这种情况。我们只需要 `AllowHtml` 特性。让我们看看带有这些特性的模型示例
public class SubmitDataModel
{
[MaxLength]
[AllowHtml]
public string LongHTMLInput { get; set; }
}
此外,如果我们希望提供一个特定的值,那么我们需要添加 `Required` 特性。特殊情况是基本(值)类型,如 `Int32`、`Boolean` 等(基本上所有结构)。由于构造过程,我们无法区分给定(满足要求)的值和默认值(例如 `Int32` 类型的 `0`)。为了避免任何错误的计算,我们应该将数据类型从值类型更改为引用类型。这可以通过在值类型后面追加 `?` 来完成。所以 `public int Id { get; set; }` 将被更改为 `public int? Id { get; set; }`。在这里,`Required` 特性再次变得有意义。
关于 MVC 中验证的一个好资源是 Henry He 在 Codeproject 上的文章。Scott Gu 还在他的博客上撰写了关于 ASP.NET MVC 2: Model Validation 的内容。
技巧 13:小心处理外键
在使用 ASP.NET MVC / Entity Framework 时,最常见的场景之一是我们有一个需要 Entity Framework 使用的数据库设置。因此,我们需要构建一个映射到现有数据库的代码。这可能会很棘手,因为 Entity Framework 遵循自己的约定,当然这些约定是可以更改的,但已设置为标准值。另一个陷阱可能是某些映射不受支持或实现起来很棘手。
有很多关于如何处理映射等的技巧和窍门,所以我们将(作为示例)详细介绍如何在代码中实现现有的一对一映射。让我们从(主导)表开始。我们将相应的实体称为 `DominantEntity`。我们假设以下结构
public class DominantEntity
{
[Key]
public virtual int Id { get; set; }
/* Some other properties */
}
现在我们构造次要实体 - 简单地称为 `MinorEntity`
public class MinorEntity
{
[Key]
[ForeignKey("DominantEntity")]
public virtual int DominantEntityId { get; set; }
public virtual DominantEntity DominantEntity { get; set; }
}
我们在这里使用 `virtual` 关键字是为了允许 Entity Framework 在可能的情况下进行延迟加载。因此,外键始终被加载,但相关的外键实体仅在请求时加载(为我们节省了一些数据库资源)。
在这种场景下,了解如何将实体的属性(即列)设置为其真实名称(如果无法应用约定)可能会非常有用。以下代码片段显示了如何通过 `OnModelCreating()` 方法重新映射 `EntityType` 实体。
public class MyDatabase : DbContext
{
/* ... */
public DbSet<EntityType> EntityTypes { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
/* ... */
modelBuilder.Entity<EntityType>().Prop(model => model.Property).HasColumnName("SampleColumnName");
modelBuilder.Entity<EntityType>().ToTable("SampleTableName");
}
}
这种专用映射的缺点是它可能导致错误,例如Multiplicity is not valid in Role ***。因为 Dependent Role refers to the key properties, the upper bound of the multiplicity of the Dependent Role must be '1'。为避免此类问题,我们需要记住在插入操作之间调用 `MyDatabase` 类的 `SaveChanges()` 方法。我们只能插入 `MinorEntity` 类型的实体,前提是引用的 `DominantEntity` 已被添加到数据库中。
有关 Entity Framework 中外键的更多信息,请参阅 CodeProject 关于 EF Code first - Add a foreign key relationship 的文章。
技巧 14:本地化操作 - 机遇与陷阱
本地化是 .NET Framework 最出色的功能之一,因为它已包含在开箱即用的服务中。我们所需要做的就是遵循一定的命名约定来命名资源文件,最终将得到一个完整的本地化应用程序。这可以应用于任何 .NET 应用程序:WinForms、WPF、WebService、WebForms……当然还有 ASP.NET MVC!
要使用本地化,我们需要两样东西
- web.config 文件中的正确条目
- 本地化后的资源文件(因此我们必须从资源文件收集所有需要本地化的资源)
资源文件的命名约定是这样的:如果我们的原始(默认)资源文件名为 String.resx,那么一个德语版本将命名为 String.de.resx。如果我们想要一个更专门的奥地利版本,我们将命名为 String.de-at.resx。现在,缺少奥地利版本的任何值都将从通用的德语版本中获取。如果通用的德语版本缺少某些值,则将从通用资源文件中获取默认值。由于资源文件会被编译然后由视图访问,因此我们需要将任何资源文件的访问修饰符设置为 `public`。
web.config 文件可能如下所示
<configuration>
<!-- ... -->
<system.web>
<!-- ... -->
<globalization culture="en-us" uiCulture="auto" />
</system.web>
</configuration>
重要的属性是 `uiCulture`。通过此属性,我们设置了用户界面的语言。值 `auto` 告诉 ASP.NET 运行时使用(从浏览器提交的)语言头。如果找不到提交的语言,将使用默认语言(来自默认资源文件)。
更有趣的是 `culture` 属性中使用 `en-us` 值。在这里,我们将内部语言设置为美国英语格式。这将处理以点为小数分隔符的数字,这就是应用此值的原因。不幸的是,JavaScript 没有本地化(即使有库)。因此,JavaScript 验证在尝试验证十进制值(如 4,5)时可能会出现错误。从某些角度来看,例如德语系统,这是一个完全有效的值,但(默认情况下)它不会被验证。为了避免这种痛苦,我们引导用户使用美国格式。这时我们的麻烦就开始了。现在客户端以美式英语验证,而服务器以德语验证(如果我们对 `culture` 使用 `auto`)。因此,我们通过在服务器端也使用美式英语来避免这种麻烦。
技巧 15:使用约束以获得更好的路由
路由是 ASP.NET MVC 的主要功能之一。为了增强错误处理程序并避免 bug,我们应该为 URL 参数使用约束。如果我们希望 ID 是一个数字值(例如 `int`),我们应该指定一个约束,如 `\d+`。约束被设置为正则表达式。正则表达式是 IT 行业中最有用的技术之一,因为它们允许我们根据模式进行快速而精确的文本搜索。本文不会介绍正则表达式。
让我们看看一些没有约束的路由可能是什么样子
routes.MapRoute(
"BlogArchive",
"Archive/{entryDate}",
new { controller = "Blog", action = "Archive" }
);
显然,所有指向 /Archive/... 的链接都将被导向 `Blog` 控制器的 `Archive` 操作。这包括像 /Archive/5 这样的 URL,以及 /Archive/abc。显然,这里我们没有约束,除了要求指定一个参数。
由于我们谈论的是日期,我们可能希望该参数采用非常特定的日期格式,例如 ISO 8601 标准(YYYY-MM-DD)。通过添加第四个参数(约束),我们可以告诉 ASP.NET MVC 路由引擎我们对有效 URL 的要求。这是最终的方法调用
routes.MapRoute(
"BlogArchive", // Route name
"Archive/{entryDate}", // Route design
new { controller = "Blog", action = "Archive" }, // Route mapping / default route values
new { entryDate = @"d{4}-d{2}-d{2}" } // Route constraints
);
我们还可以构建基于 HTTP 方法、语言和其他属性的路由约束。如果我们对现有解决方案不满意,我们甚至可以构建自己的路由约束。我们所需要做的就是构造一个实现 `IRouteConstraint` 接口的类。现在我们可以使用该类的实例作为某个属性的约束。
一篇关于整个主题的好文章可以在 Stephan Walther 的博客 上找到。
技巧 16:使用操作参数名称时要小心
当我们按照规则进行操作时,自动模型生成非常棒。例如,我们可以参考 Shawson's Code Blog 上关于 post back 时模型返回 null 的文章。为了重构示例,我们可以考虑以下控制器
public TestController : Controller
{
//
// GET: /Test/Method/5
public ActionResult Method(int id)
{
/* ... */
}
//
// POST: /Test/Method/5
[HttpPost]
public ActionResult Method(int id, DataModel specialName)
{
/* ... */
}
}
即使数据都有效且正常,ModelState也将始终无效。以下是这个具体模型可能的样子
public class DataModel
{
public int specialName { get; set; }
/* ... */
}
在这里我们看到了问题。我们已经提交了一个名为 `specialName` 的 Name-Value 对。因此,MVC 将尝试从 `specialName` 中的值生成 `DataModel` 类型的属性。这将不起作用。如果我们修改签名,使其变为 `Method(int id, DataModel otherSpecialName)`,我们将摆脱麻烦。
有时我们希望使用具有不同参数名称的现有路由,使其看起来像一个已设置的路由。使用 `Bind` 特性可以实现这一点。让我们比较这两种方法
public MyController : Controller
{
public ActionResult MyMethod([Bind(Prefix = "id")] string parameter)
{
/* ... */
}
public ActionResult MyMethod(string parameter)
{
/* ... */
}
}
如果我们只设置默认路由(记住:`{controller}/{action}/{id}`),我们将得到以下 URL 来访问这两个操作
- 第一个可以是:My/MyMethod/hello
- 第二个可以是:My/MyMethod?parameter=hello
参数名称没有太多限制。如果我们考虑上面的例子以及参数名称不允许与操作同名的规则,我们就几乎了解了如何防止有关参数名称的错误。
技巧 17:向视图添加命名空间
视图使用与应用程序本身不同的web.config,因为这会带来一些好处。如果我们想在所有视图中使用某个命名空间(例如,因为我们的模型放在该命名空间中,或者为了使用 `HtmlHelper` 实例的某个扩展方法),我们需要在位于Views子目录中的 web.config 中添加它。
以下 web.config 显示了如何添加命名空间
<configuration>
<!-- ... -->
<system.web.webPages.razor>
<pages pageBaseType="System.Web.Mvc.WebViewPage">
<namespaces>
<add namespace="System.Web.Mvc" />
<!-- ... -->
</pages>
<!-- ... -->
</system.web.webPages.razor>
</configuration>
如果我们在一个视图中更频繁地需要一个命名空间,我们可以在该视图中单独添加它。以下视图演示了这一点
@using System.Web.MyOwnNamespace
<p>Here is some paragraph!</p>
这个主题也在 StackOverflow 上进行了讨论。问题 how to import a namespace in Razor view page? 涉及在单个视图中使用命名空间。 how to add extra namespaces to razor pages? 也讨论了在 web.config 文件中添加命名空间的可能性。
技巧 18:内部操作
有时我们需要将一个方法(来自控制器)设为公共的或公开为一个操作,但实际上并不希望它直接从浏览器调用。这可以通过再次使用特性来实现。最有用的特性之一是 `ChildOnlyAction` 特性。它将使操作或控制器(当应用于控制器时)对请求不可见。访问该操作的唯一方法是从 Web 应用程序内部。
[ChildOnlyAction]
public ActionResult CanOnlyBeCalledInternal()
{
/* ... */
return PartialView();
}
通常我们会从这些操作返回部分视图。这些操作的最大优点是能够从视图中调用它们(类似于方法调用)。因此,您可以通过使用 `@Html.Action()`(并传递操作、控制器以及可能的路由值数据)将 HTML 块(部分视图)插入到视图中。这与调用 `@{Html.RenderPartial()}` 不同,后者直接渲染部分视图。区别在于第一种情况调用一个操作,然后该操作调用一个部分视图进行渲染。模型生成和数据库查询是在控制器操作中完成的。第二种情况直接调用部分视图。
有关 `Action()` 扩展方法的更多信息,请参阅 Phil Haack 的博客。
技巧 19:通过缓存提升性能
在 HTTP 世界中,有几种最佳性能实践。如果我们想坚持简单(但非常有效)的技巧,我们将首先关注请求减少。如果我们能够减少请求的数量,我们将最终获得一个更快的网页和更少的服务器负载。下一步是减少实际计算。这可以通过对算法、数据库查询等进行广泛分析来完成,但对于非常有效和健壮的解决方案,我们也可以瞄准缓存。缓存的缺点当然是所需的内存和内容的过期。内存由 ASP.NET 自动处理,这意味着少了一个问题。如果内存不足,资源将被释放,并且需要重新计算。另一方面,我们需要考虑一个良好的过期日期,以及具体缓存什么。
添加缓存的最简单方法是为控制器(因此它将应用于所有操作)或特定操作使用 `OutputCache` 特性。这将导致一个缓存的操作结果。如果 ASP.NET MVC 检测到正在调用操作,它将仅显示已缓存的输出。通常,我们还会指定计算出的内容应存储在内存中的秒数。我们假设我们要将一个操作缓存 45 秒
public CachedController : Controller
{
[OutputCache(Duration = 45)]
public ActionResult Index(int id)
{
/* ... */
}
}
当前指令仍然存在的问题是,不同的参数值(例如 `id = 2`、`id = 9` 等)将被视为相等。这意味着,如果操作被缓存且 `id` 设置为 2,那么在操作仍在缓存中的情况下,请求具有 `id` 为 9 的相同操作的输出将是相同的。
当然,我们可以通过使用其他参数来解决此类问题,例如 `VaryByParam`。因此,对于上述问题,更好的解决方案是
public CachedController : Controller
{
[OutputCache(Duration = 45, VaryByParam = "id")]
public ActionResult Index(int id)
{
/* ... */
}
}
我们可以通过使用分号分隔不同的参数名称来指定参数列表。如果我们想将每个参数都包含在此列表中,我们可以只使用 `*`。现在,如果结果是本地化的,我们仍然可能会遇到一些麻烦。在这里,我们还必须区分从该操作提供的不同语言。这可以通过使用 `VaryByHeader` 参数来实现。语言通过 `Accept-Language` 头键传递。
public CachedController : Controller
{
[OutputCache(Duration = 45, VaryByParam = "*", VaryByHeader = "Accept-Language")]
public ActionResult Index(int id)
{
/* ... */
}
}
有时我们想为多个操作(跨各种控制器)设置一个特定的设置。在这里,我们可能会陷入复制粘贴的麻烦。为了避免这种情况(并遵循 DRY:不要重复自己),我们可以使用所谓的缓存配置文件。这些配置文件就像 web.config 文件中的全局变量一样 - 只是专门用于缓存。让我们在配置文件中添加之前定义的输出缓存配置
<configuration>
<!-- ... -->
<system.web>
<!-- ... -->
<caching>
<outputCacheSettings>
<outputCacheProfiles>
<add name="Cache45Seconds" duration="45"
veryByHeader="Accept-Language" varyByParam="*"/>
</outputCacheProfiles>
</outputCacheSettings>
</caching>
</system.web>
</configuration>
现在我们可以通过引用名称 `Cache45Seconds` 来使用创建的缓存配置文件。特性调用变成
public CachedController : Controller
{
[OutputCache(CacheProfile = "Cache45Seconds")]
public ActionResult Index(int id)
{
/* ... */
}
}
这一切听起来都很简单和有吸引力,但是,我们必须始终考虑一些规则。首先:缓存持续时间真的已经太长了吗?然后:我真的应该根据所有/这些参数进行变化吗?最后,您应该始终考虑安全!切勿缓存受限制的输出。Stephen Walther 将整篇 文章献给了这个主题。
更多信息可以在 官方 ASP.NET 页面文章 上找到。Scott Hanselmann 也在他的文章 Caching in ASP.NET - VaryByParam may need VaryByHeader 中讨论了此主题。
技巧 20:重写控制器的方法
有时我们有一些相当简单的操作,它们最终只会提供一个先前编写的视图。不需要填充数据,也不需要查询任何内容。尽管如此,我们最终还是会得到一个看起来类似于
public class CustomerController : Controller
{
public ActionResult Index()
{
return View();
}
public ActionResult Details()
{
return View();
}
public ActionResult Help()
{
return View();
}
}
对于这么多不执行除调用同名视图之外的任何操作的操作来说,这真是打字过多。通过重写 `Controller` 类的 `HandleUnknownAction()` 方法,我们可以将它们全部合并
public class CustomerController : Controller
{
protected override void HandleUnknownAction(string actionName)
{
this.View(actionName).ExecuteResult(this.ControllerContext);
}
}
由于其他未知操作无论如何都会导致错误,因此在大多数情况下,可以不加思考地使用此方法。
可以在 Stephen Walther 的博客 上找到示例。
技巧 21:自定义 Membership Provider
如果我们想要使用一个更专业的 Membership 方案,我们就需要编写自己的 Membership Provider。这是处理现有数据库模式(未在 ASP.NET Membership 方案中设置)的唯一方法。CodeProject 上已经有一些关于编写自定义 Membership Provider 的文章。在本技巧中,我们将只详细介绍最有趣的点。
我们所需要做的就是继承抽象类 `MembershipProvider`。现在我们有了自己的实现,只需要为对我们有用的方法编写实际代码。
public class MyMembershipProvider : MembershipProvider
{
/* ... */
public override bool ChangePassword(string username, string oldPassword, string newPassword)
{
/* This one should really be implemented */
}
public override MembershipUser CreateUser(string username, string password, string email,
string passwordQuestion, string passwordAnswer, bool isApproved,
object providerUserKey, out MembershipCreateStatus status)
{
/* This is also quite important */
}
public override string ResetPassword(string username, string answer)
{
/* This method could be useful */
}
public override bool ValidateUser(string username, string password)
{
/* This is certainly the most important method */
}
}
编写我们自己的 Membership Provider 是一回事,但如果没有正确的 `RoleProvider`,它就会变得相当过时。在这里,我们可以实际区分用户组。
public class MyRoleProvider : RoleProvider
{
/* ... */
public override bool IsUserInRole(string username, string roleName)
{
/* This is certainly the most important method */
}
}
概念类似于 `MembershipProvider` 使用的概念。我们再次继承抽象类。现在我们已经编写了自己的实现,我们只需要告诉运行时使用我们的提供程序。这再次通过 web.config 文件完成。以下行将启用我们的 Membership Provider
<configuration>
<!-- ... -->
<system.web>
<!-- ... -->
<authentication mode="Forms">
<forms loginUrl="~/Account/LogOn" timeout="2880" />
</authentication>
<roleManager enabled="true" defaultProvider="MyRoleProvider">
<providers>
<clear/>
<add name="MyRoleProvider" type="OurNameSpace.MyRoleProvider" />
</providers>
</roleManager>
<membership defaultProvider="MyMembershipProvider">
<providers>
<clear />
<add name="MyMembershipProvider" type="OurNameSpace.MyMembershipProvider" />
</providers>
</membership>
<!-- ... --->
</system.web>
</configuration>
在 The Integrity 上可以找到一个非常好的关于编写自定义 Membership Provider 的教程。
技巧 22:如何强制使用 HTTPS
有时我们需要确保在传输敏感数据时进行加密。在网站传输中提供加密的唯一方法是使用 HTTPS 作为协议。在这里,通过使用 SSL(安全套接字层)分发适当的密钥,SSL 是在 Web 服务器和浏览器之间建立加密链接的标准安全技术。
典型情况包括在线支付系统、在线银行、身份验证、数据修改以及所有其他机密用途。为了确保使用 HTTPS 协议,我们可以使用 `RequireHttps` 特性。该特性基本上会检查我们是否已在使用安全的 HTTP 协议版本。如果我们当前使用的是 HTTP 的标准版本,该特性将通过重定向到同一操作(具有相同的参数)来响应 - 只是使用 HTTPS 协议。
[RequireHttps]
public ActionResult Login()
{
return View();
}
我们也可以通过将特性放在类定义之上,将该特性用于控制器中的所有操作。关于在调试环境中使用它的一个额外技巧。通常,我们会为有效的证书付费,因为 SSL 不仅用于加密,还用于身份验证。使用来自大型公司之一的证书,浏览器可以始终检查它正在通信的网站是否确实是它声称的网站。
如果证书是自生成的,大多数浏览器会显示警告消息。因此,使用以下代码可能会更方便
#if !DEBUG
[RequireHttps]
#endif
public ActionResult Login()
{
return View();
}
这只会对生产版本使用 HTTPS 重定向。如果您想了解有关该特性的更多信息,可以 阅读 StackOverflow 上的问题。
技巧 23:使用 T4MVC 进行强类型辅助
ASP.NET MVC 大量依赖于字符串参数和匿名对象。这两种技术使得 ASP.NET MVC 网页的编程过程非常动态,即使我们使用的是静态类型语言。虽然后者(使用匿名对象)提供了更多的自由和可能性,但前者(使用字符串作为参数)是潜在错误的来源。
T4MVC 模板的开发人员认识到了这一点,并编写了一个小型库来对抗这种错误来源。我们可以通过使用 NuGet 来安装 T4MVC 库。我们只需要在 powershell 中键入以下命令
Install-Package T4MVC
现在我们可以使用额外的扩展方法。考虑以下(通常的)方法调用示例
@Html.ActionLink("Delete this entry", "Delete", new { id = Model.Id })
这里我们调用了视图的 `HtmlHelper` 实例的 `ActionLink()` 扩展。如果我们误输入了Delete,很可能会在运行时出现异常。T4MVC 库使我们能够调用以下扩展方法
@Html.ActionLink("Delete this entry", MVC.MyControllerName.Delete(Model.Id))
现在这看起来好多了(并且像静态方法调用一样使用)。T4MVC 库在 `MVC` 命名空间中生成静态类。对于每个控制器(例如 `MyControllerNameController` 类),都会生成一个没有-Controller字符串的静态类。这些类确实包含静态字段和方法。如果操作方法包含参数,则会生成一个适当的静态方法,否则会生成一个字段。
显然,我们将能够获得智能感知和其他功能。由于强绑定,我们还将直接在页面上(而不仅仅是通过单击链接)获得异常。如果我们记住我们可以预编译视图,我们就会发现这个技巧实际上是使我们的 Web 应用程序更健壮、生产中不易出错的绝佳补充。
还包括了其他辅助功能。现在我们可以调用视图而不指定视图的名称作为字符串。考虑以下两个示例
//Old code snippet
return View("InvalidOwner");
//New way with T4MVC
return View(MVC.MyControllerName.Views.InvalidOwner);
//Old code snippet
return RedirectToAction("Details", new { id });
//New way with T4MVC
return RedirectToAction(MVC.MyControllerName.Details(id));
有一些缺点。一个是我们将没有重构支持。由于我们可以预编译我们的视图,这应该不是太大的问题(但我们必须牢记这一点!)。另一个是,这段代码是由 Visual Studio 生成的,然而,VS 只能生成/更新负责的文件,但不能保存它。所以,一旦文件被更新,我们就需要保存它。
更多信息可以在 MSDN 博客文章 A new and improved ASP.NET MVC T4 template 中找到。该项目可以在 NuGet 主页上下载。
感谢 Omar Gamil 建议添加此技巧。
使用代码
提供的代码片段应该可以正常编译,但是,由于有些示例基于实际的(MySQL)数据库,因此在不修改的情况下将无法运行。代码片段仅用于向您展示实际示例并提供实际代码,因此使用 Visual Studio 的智能感知和转到定义功能可以轻松地进行更深入的研究。如果您想设置 MySQL 数据库,则应在 web.config 文件中调整您的连接字符串设置。
兴趣点
尽管大多数技巧对于所有 MVC 开发人员来说都会是已知的,但我希望其中一两个技巧是您感兴趣的,或者至少值得写在一篇文章中。
如果您有一两个技巧要分享,请随时在评论中发布。我将非常乐意用您自己的 ASP.NET MVC 技巧和窍门来扩展本文。
历史
- v1.0.0 | 初始发布 | 2012.07.10
- v1.0.1 | 修复了一些拼写错误,包含验证码控件的图片 | 2012.07.11
- v1.1.0 | 添加了关于 HTTPS 的技巧 | 2012.07.12
- v1.1.1 | 修复了关于技巧 3 的一个拼写错误 | 2012.07.12
- v1.2.0 | 添加了关于 T4MVC 的技巧 | 2012.07.15