ASP.NET MVC 最佳实践(二)

本系列翻译自 Kazi Manzur Rashid 的博客,由于翻译水平有限,本系列可能存在误解偏差或者翻译不准的地方,建议对比原文进行阅读。由于篇幅关系,原文中的一篇文章在本系列中将拆解成多篇发布。本篇包括原文第一部分中的7-14节。如果你没有看过之前的第一部分,也许你想先看看

7. 不要在控制器中使用HttpContext类及其派生类

在你的控制器中不要引用到HttpContext以及它的派生类。这让你能比较容易地进行控制器的单元测试。如果你需要访问与HttpContext相关的对象比如User、QueryString、Cookie等,你可以使用自定义的行为过滤器(博客园老赵一篇文章对这一条进行了深入的论述,并提出了自己的解决方案,建议阅读)或者创建一些接口和容器,并把它传入控制器的构造函数。例如,下面的路由:

routes.MapRoute(
    "Dashboard",
    "Dashboard/{tab}/{orderBy}/{page}",
    new {
        controller = "Story",
        action = "Dashboard",
        tab = StoryListTab.Unread.ToString(),
        orderBy = OrderBy.CreatedAtDescending.ToString(),
        page = 1
    }
);

而控制器的行为方法则定义为:

[AcceptVerbs(HttpVerbs.Get),OutputCache(CacheProfile = "Dashboard"),UserNameFilter]
public ActionResult Dashboard(string userName, StoryListTab tab, OrderBy orderBy, int? page) {
}

UserNameFilter这个过滤器负责传递UserName:

public class UserNameFilter : ActionFilterAttribute {
    public override void OnActionExecuting(ActionExecutingContext filterContext) {
        const string Key = "userName";

        if (filterContext.ActionParameters.ContainsKey(Key)) {
            if (filterContext.HttpContext.User.Identity.IsAuthenticated) {
                filterContext.ActionParameters[Key] = filterContext.HttpContext.User.Identity.Name;
            }
        }

        base.OnActionExecuting(filterContext);
    }
}

[更新:一定要明确你已经对控制器或者是对控制器中的行为添加了 Authorize 属性,参见原文评论。]

8. 用行为控制器来转换行为方法参数

用行为过滤器来把传入值转换为你的控制器行为方法参数,再看一下 Dashboard方法的代码,我们以 Enum 类型接受 tab 和 orderBy这两个参数。

[AcceptVerbs(HttpVerbs.Get), OutputCache(CacheProfile = "Dashboard"), StoryListFilter]
public ActionResult Dashboard(string userName, StoryListTab tab, OrderBy orderBy, int? page) {
}

 

过滤器 StoryListFilter 将负责把它由 路由的值/查询字符串 转换为适当的数据类型。

public class StoryListFilter : ActionFilterAttribute {
    public override void OnActionExecuting(ActionExecutingContext filterContext) {
        const string TabKey = "tab";
        const string OrderByKey = "orderBy";

        NameValueCollection queryString = filterContext.HttpContext.Request.QueryString;

        StoryListTab tab = string.IsNullOrEmpty(queryString[TabKey]) ?
                            filterContext.RouteData.Values[TabKey].ToString().ToEnum(StoryListTab.Unread) :
                            queryString[TabKey].ToEnum(StoryListTab.Unread);

        filterContext.ActionParameters[TabKey] = tab;

        OrderBy orderBy = string.IsNullOrEmpty(queryString[OrderByKey]) ?
                            filterContext.RouteData.Values[OrderByKey].ToString().ToEnum(OrderBy.CreatedAtDescending) :
                            queryString[OrderByKey].ToEnum(OrderBy.CreatedAtDescending);

        filterContext.ActionParameters[OrderByKey] = orderBy;

        base.OnActionExecuting(filterContext);
    }
}

你也可以用自定义模型绑定器来达到同样的目的。如果要那样做的话,你需要为每个枚举创建一个独立的模型绑定器,而不是用一个行为过滤器来处理所有的枚举参数。用模型绑定器还有一个问题,一旦你注册了一个类型,在行为中它就总是被使用,而行为过滤器则是可以根据需要选择使用的。

9. 行为过滤器的位置

如果你要对你的控制器的所有行为方法应用同一个行为过滤器,你可以把这个过滤器放在控制器的定义上而不必给每个行为方法应用。如果你要对你的所有控制器应用同一个行为过滤器,你应该创建一个基控制器,对它应用该过滤器,并让所有的控制器继承这个基控制器。例如 story 控制器应该只在用户已经登陆的情况下才可以使用,并且我们需要把当前用户的用户名传入 story 控制器下的方法,另外Story控制器应该压缩返回的数据:

[Authorize, UserNameFilter]
public class StoryController : BaseController {
}

[CompressFilter]
public class BaseController : Controller {
}

但是如果继承的层次达到或者高于2层,应该另找方法来应用过滤器。最新的 Oxite 代码里有一些非常出色的动态应用过滤器的方法,我强烈推荐你去看一下。

10. 小心使用UpdateModel

我要再次强调 Justin Ethredge 在他的文章中已经提过的这个问题,开发中一定小心,避免陷进UpdateModel的陷阱里去。

11.控制器不要包含任何域逻辑

控制器应该只负责:

  • 验证输入
  • 调用Model层来为显示视图准备数据
  • 返回视图或者跳转到另一个行为

如果你在控制器中坐了其它的事情,那就说明你把它们放错了地方。你在控制器中坐的这些事情或许更应该交给模型去处理。只要你遵守了这条规则,你的每个控制器方法代码应该不会超过20到25行。 Ian Cooper 有一篇很棒的文章《Skin Controller Fat Model》,有空的时候你一定要读一下。

12. 避免使用 ViewData,尽量使用ViewData.Model

依赖于数据字典不仅使你的代码难以重构,而且你还不得不在试图中编写转换代码。实际上即使你给你的控制器的每个方法都单独编写一个类作为数据模型,那也是完全可以的。如果你觉得编写这些视图数据模型类是一项非常乏味的工作的话,你可以使用 MVCContrib 项目中 ViewDataExtensions,它包含一些用于返回强类型对象的不错的扩展。但是如果你的视图数据的数据字典中包含了多个数据类型的话,你还是没办法摆脱数据字典和他的字符串名。

13. 用 PRG 模式来修改数据

Tim Barcz, Matt Hawley, Stephen Wather 甚至 Scott Gu 都写了这方面的文章,你可以在 这里这里这里这里找到它们。这个模式的一个问题是当一项验证失败或者发生任何错误的时候,你不得不把ModelState复制到TempData里面。如果你是手动来做这件事,请不要再那样做了,你可以用行为过滤器自动处理它,就像下面这样:

[AcceptVerbs(HttpVerbs.Get), OutputCache(CacheProfile = "Dashboard"), StoryListFilter, ImportModelStateFromTempData]
public ActionResult Dashboard(string userName, StoryListTab tab, OrderBy orderBy, int? page)
{
    //Other Codes
    return View();
}

[AcceptVerbs(HttpVerbs.Post), ExportModelStateToTempData]
public ActionResult Submit(string userName, string url)
{
    if (ValidateSubmit(url))
    {
        try
        {
            _storyService.Submit(userName, url);
        }
        catch (Exception e)
        {
            ModelState.AddModelError(ModelStateException, e);
        }
    }

    return Redirect(Url.Dashboard());
}

还有行为过滤器的代码:

public abstract class ModelStateTempDataTransfer : ActionFilterAttribute {
    protected static readonly string Key = typeof(ModelStateTempDataTransfer).FullName;
}

public class ExportModelStateToTempData : ModelStateTempDataTransfer {
    public override void OnActionExecuted(ActionExecutedContext filterContext) {
        //Only export when ModelState is not valid
        if (!filterContext.Controller.ViewData.ModelState.IsValid) {
            //Export if we are redirecting
            if ((filterContext.Result is RedirectResult) || (filterContext.Result is RedirectToRouteResult)) {
                filterContext.Controller.TempData[Key] = filterContext.Controller.ViewData.ModelState;
            }
        }

        base.OnActionExecuted(filterContext);
    }
}

public class ImportModelStateFromTempData : ModelStateTempDataTransfer {
    public override void OnActionExecuted(ActionExecutedContext filterContext) {
        ModelStateDictionary modelState = filterContext.Controller.TempData[Key] as ModelStateDictionary;

        if (modelState != null) {
            //Only Import if we are viewing
            if (filterContext.Result is ViewResult) {
                filterContext.Controller.ViewData.ModelState.Merge(modelState);
            } else {
                //Otherwise remove it.
                filterContext.Controller.TempData.Remove(Key);
            }
        }

        base.OnActionExecuted(filterContext);
    }
}

MVCContrib 项目也有这个功能,但是我不喜欢他们在用一个单独类来处理的方式,我喜欢对“哪个方法输出”和“哪个方法输入”有更多的控制权。

14. 为你的视图模型创建父类层并用行为过滤器来构成公共部分

为你的视图模型类编写一个父类层,并用过滤器来构成它的公共部分。例如我正在开发的这个非常小的应用程序,我需要知道用户是否已经认证,已及用户名。

public class ViewModel {
    public bool IsUserAuthenticated {
        get;
        set;
    }

    public string UserName {
        get;
        set;
    }
}

还有行为过滤器的代码:

public class UserNameFilter : ActionFilterAttribute {
    public override void OnActionExecuting(ActionExecutingContext filterContext) {
        const string Key = "userName";

        if (filterContext.ActionParameters.ContainsKey(Key)) {
            if (filterContext.HttpContext.User.Identity.IsAuthenticated) {
                filterContext.ActionParameters[Key] = filterContext.HttpContext.User.Identity.Name;
            }
        }

        base.OnActionExecuting(filterContext);
    }
}

public class ViewModelUserFilter : ActionFilterAttribute {
    public override void OnActionExecuted(ActionExecutedContext filterContext) {
        ViewModel model;

        if (filterContext.Controller.ViewData.Model == null) {
            model = new ViewModel();
            filterContext.Controller.ViewData.Model = model;
        } else {
            model = filterContext.Controller.ViewData.Model as ViewModel;
        }

        if (model != null) {
            model.IsUserAuthenticated = filterContext.HttpContext.User.Identity.IsAuthenticated;

            if (model.IsUserAuthenticated) {
                model.UserName = filterContext.HttpContext.User.Identity.Name;
            }
        }

        base.OnActionExecuted(filterContext);
    }
}

正如你所看到的,如果在控制器里预先设定它的话,它并没有替换模型,而是在它发现模型与它符合的时候,才作为控制器的公共部分起作用。另一个好处是,由于视图只依赖于父类层,你可以不必返回具体的model,而只需要返回 View()。

继续阅读:《

               

ASP.NET MVC 最佳实践(二)》上有1条评论

  1. Pingback引用通告: ASP.NET MVC 最佳实践(一) | 所谓技术 - 小李刀刀博客

评论已关闭。