# 五、ASP.NET MVC

## 简介

[ASP.NET MVC](http://www.asp.net/mvc)引入了非常不同的开发模式。大多数人会同意，它更具可测试性，促进了职责分离——用户界面的视图，业务逻辑的控制器——并且更接近于 HTTP 协议，避免了 Web Forms 使用的一些魔法，这些魔法虽然有用，但会导致复杂性增加和性能下降。

![](img/note.png)注:Nick Harrison 简洁地为简洁系列写了[MVC](http://www.syncfusion.com/resources/techportal/ebooks/aspnetmvc)；请务必阅读它，以获得对 MVC 的良好介绍。

## 品牌化

在这里，我们将探索 MVC 提供的三种品牌机制:

*   页面布局:相当于网页表单的主页，我们将为每个租户提供不同的主页。
*   视图位置:每个租户都将其视图存储在特定的文件夹中。
*   CSS 捆绑包:每个租户都有自己的 CSS 捆绑包(特定于租户的**集合)。css** 文件)。

### 页面布局

MVC 的视图有一个类似于 Web Forms 母版页的机制，名为[页面布局](http://www.asp.net/web-pages/overview/ui,-layouts,-and-themes/3-creating-a-consistent-look)。页面布局指定了 HTML 内容的全局结构，留下“漏洞”供使用它的页面填充。默认情况下，ASP.NET MVC 对 C#视图使用**" ~/view/Shared/_ layout . cshtml "**布局，对 Visual Basic 视图使用 **"_Layout.vbhtml"** 。

使用页面布局的唯一方法是在视图中显式设置它，如本例中，使用 [Razor](http://www.asp.net/mvc/videos/mvc-3/mvc-3-razor-view-engine) :

代码示例 52

```cs
          @{
             Layout = "~/Views/Shared/Layout.cshtml";
          }

```

为了避免仅仅为了设置页面布局而一遍又一遍地重复代码，我们可以使用 MVC 的扩展性点之一[视图引擎](http://www.asp.net/mvc/videos/mvc-3/mvc-3-razor-view-engine)。因为我们将使用 [Razor](http://www.asp.net/mvc/videos/mvc-3/mvc-3-razor-view-engine) 作为首选的视图引擎，我们需要子类 [RazorViewEngine](https://msdn.microsoft.com/en-us/library/system.web.mvc.razorviewengine.aspx) ，以便注入我们特定于租户的页面布局。

我们的实现如下所示:

代码示例 53

```cs
          public sealed class MultitenantRazorViewEngine : RazorViewEngine
         {
               public override ViewEngineResult FindView(ControllerContext ctx, 
          String viewName, String masterName, Boolean useCache)
               {
                     var tenant = TenantsConfiguration.GetCurrentTenant();
                     //the third parameter to FindView is the page layout
                     return base.FindView(controllerContext, viewName, 
                           tenant.MasterPage, useCache);
               }
         }

```

为了使用我们的新视图引擎，我们需要在[视图引擎中替换现有的视图引擎。发动机](https://msdn.microsoft.com/en-us/library/system.web.mvc.viewengines.engines.aspx)集合；这应该通过从[应用程序 _ 开始](https://msdn.microsoft.com/en-us/library/ms178473%28v=vs.140%29.aspx)派生的方法来完成:

代码示例 54

```cs
          ViewEngines.Engines.Clear();
          ViewEngines.Engines.Add(new MultitenantRazorViewEngine());

```

最后，我们必须确保我们的视图从 **`ViewBag.Layout`** 设置页面布局(布局属性)，该属性映射到 [FindView](https://msdn.microsoft.com/en-us/library/system.web.mvc.iviewengine.findview.aspx) 的第三个参数中返回的内容:

代码示例 55

```cs
          @{
             Layout = ViewBag.Layout;
          }

```

### 查看位置

默认情况下，视图引擎将查找视图(**)。cshtml** 或**。vbhtml** )在一个预定义的虚拟路径集合中。我们想做的是首先在特定于租户的路径中寻找一个视图，如果在那里找不到，就返回到默认位置。我们为什么要那样？通过这种方式，我们可以将视图设计得更具有特定于租户的外观，而不仅仅是改变页面布局。

我们将使用上一页中介绍的相同视图引擎，并为此目的对其进行调整:

代码示例 56

```cs
          public sealed class MultitenantRazorViewEngine : RazorViewEngine
         {
               private Boolean pathsSet = false;

               public MultitenantRazorViewEngine() : this(false) { }
                     public MultitenantRazorViewEngine(Boolean usePhysicalPathsPerTenant)
               {
                     this.UsePhysicalPathsPerTenant = usePhysicalPathsPerTenant;
               }

               public Boolean UsePhysicalPathsPerTenant { get; private set; }

               private void SetPaths(ITenantConfiguration tenant)
               {
                     if (this.UsePhysicalPathsPerTenant)
                     {
                           if (!this.pathsSet)
                           {    
                                 this.ViewLocationFormats = new String[]
                                 {
                                      String.Concat("~/Views/",
                                            tenant.Name, "/{1}/{0}.cshtml"),
                                      String.Concat("~/Views/",
                                             tenant.Name, "/{1}/{0}.vbhtml")
                                 }                                                    .Concat(this.ViewLocationFormats).ToArray();
                                 this.pathsSet = true;
                           }
                     }
               }

               public override ViewEngineResult FindView(ControllerContext ctx, 
                     String viewName, String masterName, Boolean useCache)
               {
                      var tenant = TenantsConfiguration.GetCurrentTenant();
                     //the third parameter to FindView is the page layout
                     return base.FindView(controllerContext, viewName, 
                           tenant.MasterPage, useCache);
                     }
               }

```

这要求视图引擎配置有 **`usePhysicalPathsPerTenant`** 设置:

代码示例 57

```cs
          ViewEngines.Engines.Add(new MultitenantRazorViewEngine(true));

```

### CSS 包

[CSS 捆绑和缩小](http://www.asp.net/mvc/overview/performance/bundling-and-minification)集成在 ASP 的两大口味中。网络表单和 MVC。在网络表单中，因为它有主题机制(参见主题和皮肤)，它可能不太用于品牌化，但是 MVC 没有类似的机制。

一个 CSS 包由一个名称和一组**组成。css** 文件位于任意数量的文件夹中。我们将为每个注册的租户创建一个名称相同的包，该包包含租户配置的 **~/Content** 文件夹下的 **`Theme`** 属性中命名的文件夹中的所有文件。以下是如何:

代码示例 58

```cs
          public static void RegisterBundles(BundleCollection bundles)
         {
               foreach (var tenant in TenantsConfiguration.GetTenants())
               {
                     var virtualPath = String.Format("~/{0}", tenant.Name);
                     var physicalPath = String.Format("~/Content/{0}",
                          tenant.Theme);
                     if (!BundleTable.Bundles.Any(b => b.Path == virtualPath))
                     {
                           var bundle = new StyleBundle(virtualPath)
                                 .IncludeDirectory(physicalPath, "*.css");
                           BundleTable.Bundles.Add(bundle);
                     }
               }
         }

```

注册租户后需要调用此方法:

代码示例 59

```cs
          RegisterBundles(BundleTable.Bundles);

```

租户**“ABC . com”**将获得一个名为**“~/ABC . com”**包含所有**的 CSS 包。**“~/Content/ABC . com”**下的 css** 文件。

然而，这还不是全部；为了将 CSS 包实际添加到视图中，我们需要在视图上添加一个显式调用，如下所示:

代码示例 60

```cs
          @Styles.Render("~/abc.com")

```

然而，如果我们要硬编码租户的名字，这将无法扩展。这对于特定于租户的布局页面来说是可以的，但是对于通用视图来说就不行了。我们需要的是一种机制，可以根据每个请求自动提供正确的包名。幸运的是，我们可以通过一个[动作过滤器](https://msdn.microsoft.com/en-us/library/gg416513(VS.98).aspx)来实现这一点:

代码示例 61

```cs
          public sealed class MultitenantActionFilter : IActionFilter
         {
               void IActionFilter.OnActionExecuted(ActionExecutedContext ctx) { }

               void IActionFilter.OnActionExecuting(ActionExecutingContext ctx)
               {
                     var tenant = TenantsConfiguration.GetCurrentTenant();
                     ctx.Controller.ViewBag.Tenant = tenant.Name;        
                     ctx.Controller.ViewBag.TenantCSS = String.Concat("~/", 
                           filterContext.Controller.ViewBag.Tenant);
               }
         }

```

这个动作过滤器实现了 [IActionFilter](https://msdn.microsoft.com/en-us/library/system.web.mvc.iactionfilter%28v=vs.118%29.aspx) 界面，它所做的只是在[viewpag](https://msdn.microsoft.com/en-us/library/system.web.mvc.viewpage.viewbag%28v=vs.118%29.aspx)集合中注入两个值，租户名称( **`Tenant`** )和 CSS 包的名称( **`TenantCSS`** )。动作过滤器可以通过多种方式注册，其中之一是[全局过滤器。过滤器](https://msdn.microsoft.com/en-us/library/system.web.mvc.globalfilters.filters%28v=vs.118%29.aspx)集合:

代码示例 62

```cs
          GlobalFilters.Filters.Add(new MultitenantActionFilter());

```

打开此选项后，我们只需要在视图上添加自定义的特定于租户的捆绑包:

代码示例 63

```cs
          @Styles.Render(ViewBag.TenantCSS)

```

## 安全

我们可能希望限制某些租户的某些控制器操作。MVC 提供了一个名为[授权过滤器](https://msdn.microsoft.com/en-us/library/gg416513%28VS.98%29.aspx)的钩子，可以用来允许或拒绝对给定控制器或动作方法的访问。有一个基本实现， [AuthorizeAttribute](https://msdn.microsoft.com/en-us/library/system.web.mvc.authorizeattribute(v=vs.98).aspx) ，它只在当前用户通过身份验证时才授予访问权限。实现允许可扩展性，这就是我们要做的:

代码示例 64

```cs
          [Serializable]
         [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, 
               AllowMultiple = false, Inherited = true)]
          public sealed class AllowedTenantsAttribute : AuthorizeAttribute
         {
               public AllowedTenantsAttribute(params String [] tenants)
               {
                     this.Tenants = tenants;
               }

               public IEnumerable<String> Tenants { get; private set; }

               protected override Boolean AuthorizeCore(HttpContextBase ctx)
               {
                     var tenant = TenantsConfiguration.GetCurrentTenant();
                     return this.Tenants.Any(x => x == tenant.Name);
               }
         }

```

当应用于以一个或多个租户为参数的控制器方法或类时，它将只授予指定租户访问权限:

代码示例 65

```cs
          public class SomeController : Controller
         {
               [AllowedTenants("abc.com")]
               public ActionResult SomeThing()
               {
                     //this can only be accessed by abc.com
                     //return something     
               }

               [AllowedTenants("xyz.net", "abc.com")]
               public ActionResult OtherThing()
               {
                     //this can be accessed by abc.com and xyz.net
                     //return something
               }
         }

```

下一章描述了另一种无需代码就能限制资源访问的技术。

## 单元测试

测试 MVC 控制器和动作很简单。在多租户应用程序的环境中，正如我们在本书中所讨论的，我们只需要根据正确的租户识别策略来设置我们的测试平台。例如，如果你正在使用 [NUnit](http://nunit.org) ，你会在每次测试之前使用一个用 [SetUpFixtureAttribute](http://nunit.org/index.php?p=setupFixture&r=2.6.3) 修饰的方法来完成:

代码示例 66

```cs
          [SetUpFixture]
          public static class MultitenantSetup
         {
               [SetUp]
               public static void Setup()
               {
                     var req = new HttpRequest(
                           filename: String.Empty,
                           url: "http://abc.com/",
                           queryString: String.Empty);
                     req.Headers["HTTP_HOST"] = "abc.com";
                     //add others as needed

                     var response = new StringBuilder();
                     var res = new HttpResponse(new StringWriter(response));

                     var ctx = new HttpContext(req, res);

                     var principal = new GenericPrincipal(
                           new GenericIdentity("Username"), new [] { "Role" });
                     var culture = new CultureInfo("pt-PT");

                     Thread.CurrentThread.CurrentCulture = culture;
                     Thread.CurrentThread.CurrentUICulture =  culture;

                     HttpContext.Current = ctx;
                     HttpContext.Current.User = principal;
               }
         }

```