原文地址:ASP.NET MVC Tip #31 – Passing Data to Master Pages and User Controls
原文作者:swalther

本文译者:QLeelulu

摘要:
在这个Tip中,我会讨论给MasterPages和UserControls传递数据的4种策略。我会讲解通过code-behind类、通过使用ActionFilter、通过调用局部方法、和通过使用抽象的Controller基类来传递数据。我推荐使用最后一种方法。

 

在这个Tip中,我推荐一种传递数据到MasterPages和UserControls的方法。但在提出我的建议前,我会先讲解一下这个问题的几种解决方法。

The Problem

想象一下你要使用ASP.NET MVC框架来开发一个movie database application。你决定要在该应用的每一个页面上都显示一个电影分类的列表,这样,用户就可以方便的导航到他喜欢的分类。一旦你想该电影分类列表显示在每一个页面,很自然的就会想到在MasterPage中显示这个列表。
你也决定在某些页面上显示一些热门的电影列表,但不是显示在所有的页面上。这个热门的电影列表是从数据库中随机的取出来的。你决定要通过用户控件来实现:就叫 FeaturedMovies control (见图 1).

图 1 – The Movie Database Application
clip_image002_thumb

问题就出现在这里。你需要在程序中给每一个页面的母版页传递电影分类列表数据。你需要给程序中的某些特定的页面的热门电影用户控件传递热门电影列表数据。你怎么实现这个呢?

Using a Code-Behind Class

最通常的做法,但是是错误的,就是在code-behind class 中为你的MasterPage和FeaturedMovies用户控件取数据来解决这个问题。Listing 1 中的MasterPage显示code-behind class中的叫做Categories属性的电影分类列表。

Listing 1 – Site.Master

<%@ Master Language="C#" AutoEventWireup="true" CodeBehind="Site.master.cs" Inherits="Solution1.Views.Shared.Site" %>
<%@ Import Namespace="Solution1.Models" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
    <title>Movies</title>
    <link href="http://www.cnblogs.com/Content/Site.css" rel="stylesheet" type="text/css" />
</head>

<body>
<div class="page">

    <div id="header">
        <h1>Movie Database Application</h1>
    </div>

    <div id="main">
 
        <div class="leftColumn">
        <ul>
        <% foreach (MovieCategory c in this.Categories)
           { %>
            <li> <%= Html.ActionLink(c.Name, "Category", new {id=c.Id} )%></li>
        <% } %>
        </ul>
        </div>
                
        <div class="rightColumn">
        <asp:ContentPlaceHolder ID="MainContent" runat="server" />
        </div>

        <br style="clear:both" />
        <div id="footer">
            Movie Database Application &copy; Copyright 2008
        </div>
    </div>
</div>
</body>
</html>

这个MasterPage的code-behind class在Listing 2 中。注意在Listing 2 中是直接通过LINQ2SQL来取数据的。

Listing 2 – Site.Master.cs

using System.Collections.Generic;
using System.Linq;
using Solution1.Models;

namespace Solution1.Views.Shared
{
    public partial class Site : System.Web.Mvc.ViewMasterPage
    {
        protected IEnumerable<MovieCategory> Categories
        {
            get
            {
                var dataContext = new MovieDataContext();
                return from c in dataContext.MovieCategories select c;
            }
        }
    }

}

 

你同样可以为FeaturedMovies 用户控件来取数据。在FeaturedMovies code-behind class 从数据库中取热门电影的列表数据。

那么,为什么这错了呢?这当然好像一个简单的解决办法。它正常工作了,为什么还要抱怨?

这个解决方案的问题是MasterPage的code-behind class 中的代码是不可测试的。你不可以很方便的为Site类写单元测试,因为Site类是继承自ViewMasterPage类,而ViewMasterPage类继承自Page类。The Page class relies on the HTTP Context object and all hope of isolating your code so that it can be tested goes away.

在开发ASP.NET MVC应用的时候,你应该尽量避免在你的程序中在code-behind class 中处理逻辑,尝试将所有的东西都放回到Controllers中。Controllers被设计为可测试的。

Using an Action Filter

所以让我们以另一种途径来解决这个传递数据给MasterPage或者view的问题。在这一节,我们创建一个ActionFilter来修改传递给view的ViewData。这个方法你可以通过给controller添加一个或者多个action filter来修改由controller传递给view的ViewData。

这个ActionFilter,命名为[Partial] ,如Listing 3所示。

Listing 3 – ActionFilters\PartialAttribute.cs

using System;
using System.Reflection;
using System.Web.Mvc;

namespace Solution2.ActionFilters
{
    public class PartialAttribute : ActionFilterAttribute
    {
        private string _partialClassName;

        public PartialAttribute(string partialClassName)
        {
            _partialClassName = partialClassName;
        }

        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var viewData = (filterContext.Controller as Controller).ViewData;
            ExecutePartial(_partialClassName, viewData);
        }

        private void ExecutePartial(string partialName, ViewDataDictionary viewData)
        {
            // Get partial type
            var partialType = Type.GetType(partialName, true, true);
            var partial = Activator.CreateInstance(partialType);

            // Execute all public methods
            var methods = partialType.GetMethods();
            foreach (MethodInfo method in methods)
            {
                var pams = method.GetParameters();
                if (pams.Length > 0 && pams[0].ParameterType == typeof(ViewDataDictionary))
                    method.Invoke(partial, new object[] { viewData });

            }
        }

    }
}

 

当你添加[Partial] 到一个controller action的时候,这个ation filter会附加一些数据到view data中去。例如,有可以使用[Partial] 特性来添加电影分类列表的数据到view data中去以便在master page中显示。你也可以使用[Partial] 特性来添加热门电影列表到view data 中以使在FeaturedMovie 用户控件中得到该数据。

[Partial] 特性通过一个类名,实例化这个类,然后执行类里面所有的public方法(每一个方法都包含一个ViewDataDictionary参数),Listing 4 中的controller说明了你可以怎样使用[Partial] action filter来为不同的controller返回不同的ViewData。

Listing 4 – HomeController.cs

using System.Linq;
using System.Web.Mvc;
using Solution2.ActionFilters;
using Solution2.Models;

namespace Solution2.Controllers
{
    [Partial("Solution2.Partials.Master")]
    public class HomeController : Controller
    {

        [Partial("Solution2.Partials.Featured")]
        public ActionResult Index()
        {
            return View();
        }

        public ActionResult Category(int id)
        {
            var dataContext = new MovieDataContext();
            var movies = from m in dataContext.Movies where m.CategoryId == id select m;
            return View("Category", movies);
        }
    }
}

注意到HomeController它本身是添加了[Partial] action filter的。由于[Partial] action filter应用到类上,在HomeController里面的每一个action执行的时候这个action filter都会执行的。在类级别上应用[Partial] 特性来为master page提供view data。

类级别上的[Partial]特性添加电影分类列表到view data中。[Partial]执行Solution2.Partials.Master 类中的方法,如Listing 5 所示。

Listing 5 – Master.cs

using System.Linq;
using System.Web.Mvc;
using Solution2.Models;

namespace Solution2.Partials
{
    public class Master
    {
        public void AddViewData(ViewDataDictionary viewData)
        {
            var dataContext = new MovieDataContext();
            var categories = from c in dataContext.MovieCategories select c;
            viewData["master"] = categories; 
        }
    }
}

 

AddViewData() 方法将categories 添加到key为"master"的view data dictionary中。master page从view data 中取出categories 并显示。

[Partial] 也可以添加到特定的action上,例如Listing 4 中的Index()方法。

然而这种从controller中传递数据给母版页和用户控件的解决方案错在哪里呢?这种方法的优于前面一种方法的地方在于他将获取数据的逻辑放回到controller中来处理了。ViewData在controller action 调用的时候会被修改。

无论怎样,这个解决方案还是挺不错的。通过使用[Partial] 特性,你可以为view data dictionary 添加更多的数据。例如,如果你决定你要添加一个新的用户控件到某一个页面,而这个新的用户控件需要一个不同的数据集,你可以很简单的添加一个新的[Partial] 特性到正确的controller action上来添加新的数据到view data dictionary中去。

遗憾的是,只是一点点的遗憾,这个解决方案不是很容易进行单元测试。当你在一个单元测试里面执行action方法的时候ActionFilter并不会执行。所以,我们需要寻找一个不同的策略来解决这个问题。

Calling Partial Methods Directly

让我们进入到这个问题的第三个解决方案中。在这一节中,我们尝试通过直接在controller中来获取数据然后传递给母版页和用户控件来解决这个问题。在Listing 6 中是我们修改后的HomeController的代码。

Listing 6 – HomeController.cs (with partials logic)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Solution3.Models;
using Solution3.Partials;

namespace Solution3.Controllers
{
    public class HomeController : Controller
    {
        public HomeController()
        {
            Master.AddViewData(this.ViewData);
        }


        public ActionResult Index()
        {
            Featured.AddViewData(this.ViewData);
            return View();
        }

        public ActionResult Category(int id)
        {
            var dataContext = new MovieDataContext();
            var movies = from m in dataContext.Movies where m.CategoryId == id select m;
            return View("Category", movies);
        }
    }
}

注意到Listing 6 中的HomeController有一个构造函数。在构造函数中调Master.AddViewData() 来改变controller action中返回的view data。这个方法要在母版页中显示的view data。

Index()方法也改变了。在Index()方法里面,调用了Featured.AddViewData() 方法。这个方法为FeaturedMovies 用户控件添加必需的view data。由于FeaturedMovies 用户控件只在Index视图中呈现,而不在Categorys视图中呈现,所以不在构造函数中调用Featured.AddViewData() 方法。

这个解决方案的优点是非常容易进行单元测试。当你调用Index()方法,view data同时被Master和Featured的部分方法改变了。也就是说,你可以容易的测试你传递给母版页和用户控件的view data是否是正确的。

那么,这个解决方案错在哪里了呢?添加view data的所有逻辑都已经放到controller类中来处理了。这个解决方案已经比前面的两个方案要好很多了。这个解决方法的唯一的问题是它违背了单一责任原则。

根据单一责任原则,代码应该只有一个单一的理由去改变(code should have only a single reason to change)。然而,我们有很多原因要去改变Listing 8 中的Index()方法。如果我们也决定添加一个新的用户控件到Index视图中,而这个新的用户控件显示一个新的数据集,然后我们就需要去修改Index() action了。

单一责任制原则背后的目的是你永远不要去改变已经在运作中的代码。改变代码通常意味着为你的应用带入一个bug。我们需要寻找一些途径来添加新的view data而不用修改我们的controller action。

Using Abstract Base Classes

这里是我对于这个问题的最后一个解决方案:我们将使用抽象的基类来改变从controller action返回来的view data。我现在要警告你这个是复杂的。我们需要创建好几个类。然而,每一个类都是单一职责的。每一个类都只是负责一种类型的view data 而已(见图2)。

Figure 2 – Class Hierarchy

clip_image004_thumb

我们将创建一个抽象基类,命名为ApplicationController ,改变view data divtionary来为我们的母版页添加所需的所有的view data(见Listing 7)。这个ApplicationController 在我们的程序中作为每一个controller的基类,而不单单是HomeController。

Listing 7 – ApplicationController

using System.Web.Mvc;
using Solution4.Partials;

namespace Solution4.Controllers
{
    public abstract class ApplicationController : Controller
    {
        public ApplicationController()
        {
            Master.AddViewData(this.ViewData);
        }
    }
}

下一步,我们将要创建一个命名为HomeControllerBase 的抽象基类(见Listing 8).这个类包含了通常出现在HomeController的所有的应用逻辑。我们将会重写action方法来为特定的用户控件添加需要的view data。

Listing 8 – HomeControllerBase.cs

using System.Linq;
using System.Web.Mvc;
using Solution4.Models;

namespace Solution4.Controllers.Home
{
    public abstract class HomeControllerBase : ApplicationController
    {
        public virtual ActionResult Index()
        {
            return View("Index");
        }

        public virtual ActionResult Category(int id)
        {
            var dataContext = new MovieDataContext();
            var movies = from m in dataContext.Movies where m.CategoryId == id select m;
            
            return View("Category", movies);
        }
    }
}

对于每一个用户控件,我们将会需要创建一个额外的抽象类。对于FeaturedMovies 用户控件,我们将会创建一个HomeControllerFeatured 类(见Listing 9)。对于PopularMovies 用户控件,我们将会创建一个HomeControllerPopular 类(见Listing 10)。

Listing 9 – HomeControllerFeatured.cs

using System.Web.Mvc;

namespace Solution4.Controllers.Home
{
    public abstract class HomeControllerFeatured : HomeControllerBase
    {

        public override ActionResult Index()
        {
            var result = (ViewResult)base.Index();
            Partials.Featured.AddViewData(result.ViewData);
            return result;
        }
    
    }
}

 

Listing 10 – HomeControllerPopular.cs

using System.Web.Mvc;

namespace Solution4.Controllers.Home
{
    public abstract class HomeControllerPopular : HomeControllerFeatured
    {
        public override System.Web.Mvc.ActionResult Category(int id)
        {
            var result = (ViewResult)base.Category(id);
            Partials.Popular.AddViewData(result.ViewData);
            return result;
        }


    }
}

最后,我们需要添加这个层次关系的最上面一个类。我们将会创建一个HomeController 类。这个类简单的继承自上面的其中一个基类(见Listing 11)。他本身并不包含应用逻辑。

HomeController 类这个层次关系中的唯一一个不是抽象类的。由于它不是抽象类,他的controller actions 可以被全世界调用(its controller actions can be invoked by the world)。

Listing 11 – HomeController.cs

namespace Solution4.Controllers.Home
{
    public class HomeController : HomeControllerPopular
    {
    }
}

现在,你或许会受不了这么多的类。然而,这个解决方案的优点是我们已经很干净的分离了建造view data 的逻辑。每一个抽象类都具有单一的责任。我们的代码不再脆弱。

Summary

我并完全信服我自己的Tip。我仍然在尝试这通过使用action filter 来为我的master pages 和 user controls 添加view data。描述的最后一个解决,使用抽象基类,好像需要大量工作。我很好奇于这个问题的其他的解决方案。

推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架
新浪微博粉丝精灵,刷粉丝、刷评论、刷转发、企业商家微博营销必备工具"