zoukankan      html  css  js  c++  java
  • [Study Note] Maintainable MVC Series

    注:随笔是2010年3月份写的,当时不知道怎么就保存成了草稿而一直没有发布。并没有完成整个系列,回过头来,我似乎也看不太明白了,现在(2013年6月)发布一下,算是纪念吧,不过,我把发布时间改成了2012年

    [Maintainable MVC Series: Introduction]

    这篇Introduction中最吸引我的是关于 web application 的架构。

    PRESENTATION: Views, Controllers, Form Model, View Model, Handlers, Mappers, (Domain Model)

    DOMAIN: Services, (Domain Model)

    INFRASTRUCTURE: Domain Model, Repositories, Mappers, Data storage

    如此的架构比我之前的那种 Model, DAL, BLL, WebUI——所谓“三层架构”要漂亮许多,相比之下,现在所写的代码似乎如三叶虫般丑陋并且低级。

    2010-03-29 17:29

    [Maintainable MVC Series: Inversion of Control Container - StructureMap]

    其实之前一直没有搞明白什么是 Inversion of Control, IOC,中文似乎翻译做“反转控制”。至于 StructureMap,也只是听说。其实最早看到这个系列,似乎也正是因为这篇 StructureMap 的文章。

    不过这篇文章主要是讲如何将 StructureMap 和 MVC 相融合,似乎并不是我想要的,顺手捎带着去看了一些关于 StructureMap 的内容。

    看完这一篇之后,加上后续的目录,估计这一系列文章主要是介绍一些 ASP.NET Web Application 架构方面的内容,对我来说也算是一次系统的学习吧。

    2010-03-30 23:20

    [Maintainable MVC Series: View hierarchy]

    Master view

    mvc-view-hierarchy “无图无真相”,此图一出,解决了我之前对于 Master page 和其他页面之间,对于 css, javascript 一类文件引用的处理。在手头的项目里面,因为重复的引用,所以页面加载的时候往往会出现多个 css 或者是 javascript 文件。虽然似乎对页面的显示没有什么特别的副作用,但是想来会对加载的速度产生负面影响。

    // 此处需要插入代码着色

    <%@ Master Language="C#" Inherits="System.Web.Mvc.ViewMasterPage" %>
    <%@ Import Namespace="ClientX.Website.Models"%>

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8">
        <title>Site title - <asp:ContentPlaceHolder ID="TitleContent" runat="server" /></title>
    <% #if DEBUG %>
        <% Html.RenderPartial("CssDebug", ViewData.Eval("DataForCss")); %>
    <% #else %>
        <% Html.RenderPartial("CssRelease", ViewData.Eval("DataForCss")); %>
    <% #endif %>
    </head>
    <body>

        <% Html.RenderPartial("Header", ViewData.Eval("DataForHeader")); %>

        <% Html.RenderPartial("PostBackForm", ViewData.Eval("DataForPostBackForm")); %>

        <% Html.RenderPartial("Messages", ViewData.Eval("DataForMessages")); %>

        <div class="content">

            <asp:ContentPlaceHolder ID="MainContent" runat="server" />

        </div><!-- /content -->

        <% Html.RenderPartial("Footer", ViewData.Eval("DataForFooter")); %>

    <% #if DEBUG %>
        <% Html.RenderPartial("JavascriptDebug", ViewData.Eval("DataForJavascript")); %>
    <% #else %>
        <% Html.RenderPartial("JavascriptRelease", ViewData.Eval("DataForJavascript")); %>
    <% #endif %>

    </body>
    </html>

    If your site use multiple different layouts (1, 2 or 3 columns for example) it is advisable to have nested master pages. The top one still keeps the css, javascript and navigation, where the nested master pages will only contain the html that differs between the different layouts.

    Page View

    mvc-page-and-partial-views // 此处需要插入代码着色

    <%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<OverviewViewModel>" %>
    <%@ Import Namespace="ClientX.Website.Models"%>

    <asp:Content ID="TitleContentPlaceHolder" ContentPlaceHolderID="TitleContent" runat="server">
        Page title
    </asp:Content>

    <asp:Content ID="MainContentPlaceHolder" ContentPlaceHolderID="MainContent" runat="server">

        <% Html.RenderPartial("Heading", Model.Heading); %>

        <div>
            <% Html.RenderPartial("OverviewSearchForm", Model.OverviewSearchForm); %>

            <% Html.RenderPartial("Pager", Model.Pager); %>

            <table>
                <% Html.RenderPartial("TableHeader", Model.TableHeader); %>
                <%foreach (TableRowViewModel item in Model.ItemList){
                    Html.RenderPartial("TableRow", item);
                } %>
            </table>

            <% Html.RenderPartial("Pager", Model.Pager); %>
        </div>
    </asp:Content>

    strongly typed

    Never to share a view model between multiple views. Each view has it’s own tailor made view model.

    [Maintainable MVC Series: View Model and Form Model]

    Every bit of data displayed in the view has a corresponding property in the View Model.

    View Model mapper

    public ActionResult ShowItem(int id)
    {
        DomainModel item = repository.GetItem(id);

        ViewModel viewModel = mapper.MapToViewModel(item);

        return View("ShowItem", viewModel);
    }

    public ViewModel MapToViewModel(DomainModel domainModel)
    {
        ViewModel viewModel = new ViewModel
        {
            Id = domainModel.Id,
            Name = domainModel.Name,
            Description = domainModel.Description
        };

        return viewModel;
    }

    AutoMapper

    AutoMapper is a liberary with the purpose of mapping properties of a complex type to simple type like a view model.

    Besides saving you of writing lots of code it also has test facilities to warn you if your target model (the view model) has new properties which have no counterpart in the source model (the domain model).

    public ViewModel MapToViewModel(DomainModel domainModel)
    {
        Mapper.CreateMap<DomainModel, ViewModel>();

        ViewModel viewModel = Mapper.Map<DomainModel, ViewModel>(domainModel);

        return viewModel;
    }

    Form Model

    to keep things clear we only post form models.

    public class ViewModel {

        public FormModel Data { get; set; }

        public string NameLabel { get; set; }

        public string DescriptionLabel { get; set; }

        public string CountryLabel { get; set; }

        public SelectList Countries { get; set; }

        public string CountryChooseOption { get; set; }

    }

    <% using(Html.BeginForm()) {%>
        <%= Html.AntiForgeryToken() %>
        <label>
            <%= Html.Encode(Model.NameLabel) %>:
            <%= Html.TextArea("Name", Model.Data.Name) %>
        </label>
        <label>
            <%= Html.Encode(Model.DescriptionLabel) %>:
            <%= Html.TextArea("Description", Model.Data.Description) %>
        </label>
        <label>
            <%= Html.Encode(Model.CountryLabel) %>:
            <%= Html.DropDownList("Country", Model.Countries, Model.CountryChooseOption) %>
        </label>
        <input type="submit" value="Save">
    <% } %>

    As you can see (?) the values of the input fields are populated from the form model, while representation data like the dropdown list items and labels come from the view model.

    Posting the form model

    A bonus of having all properties correspond to a form field is that all of them will be binded automatically by MVC. No need for custom binding here.

    [AcceptVerbs(HttpVerbs.Post), ValidateAntiForgeryToken]
    public ActionResult UpdateItem(int id, FormModel formModel)
    {
        DomainModel item = repository.GetItem(id);

        item.Name = formModel.Name;
        item.Description = formModel.Description;

        repository.SaveItem(item);

        return RedirectToAction("UpdateItem", new{ id });
    }

    Form handler

    The form handler handles updating the domain model from the form model.

    CQRS (Command Query Responsibility Segregation)

    View models and mapping for querying, form model and form handlers for command handling.

    Validation

    form model validation to make sure the posted data is what we want in terms of being required.

    client-side validation is done easily.

    Handling the first level protection of validating the form model can be done in the controller or in the form handler. Additional levels of validation are fed back to the controller by the form handler.

    2010-03-31 19:41

    [Maintainable MVC Series: Poor man’s RenderAction]

    Html.RenderAction and Html.Action

    Both of these methods allow you to call into an action method from a view and output the result of the action in place within the view.

    Html.RenderAction will render the result directly to the Response (which is more efficient if the action returns a large amount of HTML)

    Html.Action returns a string with the result

    [ChildActionOnly] ChildActionOnlyAttribute indicates that this action should not be callable directly via the URL. It’s not required for an action to be callable via RenderAction.

    • Passing Values With RenderAction
    • Cooperating with the ActionName attribute
    • Cooperating with Output Caching

    <%= Html.Action(“Menu”) %>

    [ChildActionOnly]
    public ActionResult Menu() {

        NavigationData navigationData = navigationService.GetNavigationData();
        MenuViewModel menuViewModel = mapper.GetMenuViewModel(navigationData);

        return PartialView(menuViewModel);
    }

    <% Html.RenderPartial(“Menu”, ViewData.Eval(“DataForMenu”)); %>

    The attribute

    [AttributeUsage(AttributeTarget.Class |AttributeTargets.Method, AllowMultiple = false)]

    public sealed class DataForMenuAttribute : ActionFilterAttribute

    {

        private readonly INavigationService navigationService;

        private readonly IViewModelMapper mapper;

        public DataForMenuAttribute() : this(ObjectFactory.GetInstance<INavigationService>(), ObjectFactory.GetInstance<IViewModelService>())

        {

        }

        public DataForMenuAttribute(INavigationService navigationService, IViewModelMapper mapper)

        {

            this.NavigationService = navigationService;

            this.mapper = mapper;

        }

        …

        public override void OnActionExecuting(ActionExecutingContext filterContext)

        {

            // Work only for GET request

            if ( filterContext.Request.Context.HttpContext.Request.RequestType != “GET” )

                return;

            // Do not work with AjaxRequests

            if ( filterContext.RequestContext.HttpContext.Request.IsAjaxRequest() )

                return;

       

            NavigationData navigationData = navigationService.GetNavigationData();

            MenuViewModel menuViewModel = mapper.GetMenuViewModel(navigationData);

            filterContext.Controller.ViewData[“DataForMenu”] = menuViewModel;

        }

    }

    Unit testing the attribute

    [TestFixture]

    public class DataForMenuAttributeTests

    {

        private RhinoAutoMocker<DataForMenuAttribute> autoMocker;

        private DataForMenuAttribute attribute;

       

        private SomeController controller;

        private ActionExecutingContext context;

        private HttpRequestBase httpRequestMock;

        private HttpContextBase httpContextMock;

        [SetUp]

        public void SetUp()

        {

            StructureMapBootstrapper.Restart();

           

            autoMocker = new RhinoAutoMocker<DataFormenuAttribte>(MockMode.AAA);

            attribute = autoMocker.ClassUnderTest;

            controller = new SomeController();

            httpRequestMock = MockRepository.GenerateMock<HttpRequestBase>();

            httpContextMock = MockRepository.GenerateMock<HttpContextBase>();

            httpContextMock.Expect(x => x.Request).Repeat.Any().Return(httpRequestMock);

            context = new ActionExecutingContext(

                    new ControllerContext(httpContextMock, new RouteData(), Controller),

                    MockRepository.GenerateMock<ActionDescriptor>(),

                    new Dictionary<string, object>() );

        }

        [Test]

        public void DataForMenuAttributeShouldNotSetViewDataForPostRequest()

        {

            // Arrange

            httpRequestMock.Expect(r => r.RequestType).Return(“POST”);

            // ACT

            attribute.OnActionExecting(context);

            // Assert

            Assert.That(controller.ViewData[“DataForMenu”], Is.Null);

        }

        [Test]

        public void DataForMenuAttributeShouldCallGetNavigationData()

        {

            // Arrange

            httpRequestMock.Expect(r => r.RequestType).Return(“GET”);

            httpRequestMock.Expect(r => r[“X-Requested-With”].Return(string.Empty));

            NavigationData navigationData = new NavigationData();

            autoMocker.Get<INavigationService>().Expect(x => x.GetNavigationData()).Return(navigationData);

            // Act

            attribute.OnActionExecuting(context);

           

            // Assert

            autoMocker.Get<INavigationService>().VerifyAllExpectations();

        }

        [Test]

        public void DataForMenuAttributeShouldCallGetMenuViewMode()

        {

            // Arrange

            httpRequestMock.Expect(r => r.RequestType).Return(“GET”);

            httpRequestMock.Expect(r => r[“X-Requested-With”]).Return(string.Empty);

           

            NavigationData navigationData = new NavigationData();

            MenuViewModel menuViewModel = new MenuViewModel();

            autoMocker.Get<INavigationService>().Expect(x => x.GetNavigationData()).Return(navigationData);       

            autoMocker.Get<IViewModelMapper>().Expect(x => x.GetMenuViewModel(navigationData)).Return(menuViewModel);

           

            // Act

            attribute.OnActionExecuting(context);

           

            // Assert

            autoMocker.Get<IViewModelMapper>().VerifyAllExpectations();

        }

        [Test]

        public void DataForMenuAttributeShouldSetMenuViewDataToGetMenuViewModelResult()

        {

            // Arrange

            httpRequestMock.Expect(r => r.RequestType).Return(“GET”);

            httpRequestMock.Expect(r => r[“X-Requested-With”]).Return(string.Empty);

            MenuViewModel menuViewModel = new MenuViewModel();

            autoMocker.Get<IViewModelMapper>().Expect(x => x.GetMenuViewModel(Arg<NavigationData>.Is.Anything)).Return(menuViewModel);

            // Act

            attribute.OnActionExecuting(context);

            // Assert

            Assert.That(controller.ViewData[“DataForMenu”], Is.EqualTo(menuViewModel));

        }

        [Test]

        public void ExampleControllerShouldHaveDataForMenuAttribute()

        {

            Assert.That(Attribute.GetCustomAttribute(typeof(ExamplerController), typeof(DataForMenuAttribute)), Is.Not.Null);

        }

    }

    Make sure you have this test for every controller that needs the attribute!

    [Maintainable MVC Series: Post-Redirect-Get pattern]

    Post-Redirect-Get pattern, PRG

    ModelStateToTempDataAttribute

    public class ModelStateToTempDataAttribute : ActionFilterAttribute

    {

        public const string TempDataKey = “_MvcContrib_ValidationFailures_”;

       

        public override void OnActionExecuted(ActionExecutedContext filterContext)

        {

            ModelStateDictionary modelState = filterContext.Controller.ViewData.ModelState;

           

            ControllerBase controller = filterContext.Controller;

            if (filterContext.Result is ViewResult)

            {

               // If there are failures in tempdata, copy them to the modelstate

                CopyTempDataToModelState(controller.ViewData.ModelState, controller.TempData);

                return;

            }

            // If we’re redirecting and there are errors, put them in tempdata instead

           // (so they can later be copied back to modelstate)

            if ((filterContext.Result is RedirectToRouteResult || filterContext.Result is RedirectResult) && !modelState.IsValid)

            {

                CopyModelStateToTempData(controller.ViewData.ModelState, controller.TempData);

            }

        }

        private void CopyTempDataToModelState(ModelStateDictionary modelState, TempDataDictionary tempData)

        {

            if(!tempData.ContainsKey(TempDataKey))

            {

                return;

            }

           

            ModelStateDictionary fromTempData = tempData[TempDataKey] as ModelStateDictionary;

            if (fromTempData == null)

            {

                return;

            }

            foreach(keyValuePair<string, ModelState> pair in fromTempData)

            {

                if (modelState.ContainsKey(pair.Key))

                {

                    modelState[pair.Key].Value = pair.Value.Value;

                    foreach(ModelError error in pair.Value.Errors)

                    {

                        modelState[pair.Key].Errors.Add(error);

                    }

                }

                else

                {

                    modelState.Add(pair.Key, pair.Value)

                }

            }

        }

        private static void CopyModelStateToTempData(ModelStateDictionary modelState, TempDataDictionary tempData)

        {

            tempData[TempDataKey] = modelState;

        }

    }

    The attribute only saves ModelState to TempData if there are validation errors. And if the next action returns a view, the ModelState is retrieved from TempData. TempData itself is a wrapper around Session-State in which objects are only present until the next request.

    storing Session-State

    • InProc
    • StateServer
    • SQLServer
    • Custom
    • Off

    [Maintainable MVC Series: Binding]

    SmartBinder

    public interface IFlteredModelBinder : IModelBinder

    {

        bool IsMatch(Type modelType);

        new BindResult BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext);

    }

    public class SmartBinder : DefaultModelBinder

    {

        private readonly IFilteredModelBinder[] filteredModelBinders;

        public SmartBinder(IFilteredModelBinder[] filteredModelBinders)

        {

            this.filteredModelBinders = filteredModelBinders;

        }

        public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)

        {

            foreach (var filteredModelBinder in filteredModelBinders)

            {

                if (filteredModelBinder.IsMatch(bindingContext.ModelType))

                {

                    BindResult result = filteredModelBinder.BindModel(controllerContext, bindingContext);

                    bindingContext.ModelState.SetModelValue(bindingContext.ModelName, result.ValueProviderResult);

                    return result.Value;

                }

            }

            return base.BindModel(controllerContext, bindingcontext);

        }

    }

    public class BindResult

    {

        public object Value { get; private set; }

        public ValueProviderResult ValueProviderResult { get; private set; }

        public BindResult(object value, ValueProviderResult valueProviderResult)

        {

            this.Value = value;

            this.ValueProviderResult = valueProviderResult ?? new ValueProviderResult(null, string.Empty, CultureInfo.CurrentCulture);

        }

    }

    Setting it up with StructureMap

    // global.asax.cs

    ModelBinders.Binders.DefaultBinder = ObjectFactory.GetInstance<SmaterBinder>();

    public class BinderRegistry : Registry

    {

        public BinderRegistry()

        {

            For<IFilteredModelBinder>().Add<EnumBinder<SomeEnumeration>>().Ctor<SomeEnumeration>().Is(SomeEnumeration.FirstValue);

            For<IFilteredModerBinder>().Add<EnumBinder<AnotherEnumeration>>().Ctor<AnotherEnumeration>().Is(AnotherEnumeration.FifthValue);

        }  

    }

    EnumBinder

    public class EnumBinder<T> : DefaultModelBinder, IFilteredModelBinder

    {

        private readonly T defaultValue;

        public EnumBinder(T defaultValue)

        {

            this.defaultValue = defaultValue;

        }

        #region IFilteredModelBinder mebers

        public bool IsMatch(Type modelType)

        {

            return modelType == typeof(T);

        }

        BindResult IFilteredModelBinder.BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)

        {

            T result = bindingContext.ValueProvider[bindingContext.ModelName] == null ? defaultValue : GetEnumValue(defaultValue, bindingContext.ValueProvider[bindingContext.Modelname].AttemptedValue);

            return new BindResult(result, null);

        }

        #endregion

        private static T GetEnumValue(T defaultValue, string value)

        {

            T enumType = defaultValue;

            if ( (!String.IsNullOrEmpty(value)) && (Contains(typeof(T), value)))

            {

                enumType = (T)Enum.Parse(typeof(T), value, true);

            }

            return enumType;

        }

        private static bool Contains(Type enumType, string value)

        {

            return Enum.GetName(enumType).Contains(value, StringComparer.OrdinalIgnoreCase);

        }

    }

    Testing the binder

    public enum TestEnum

    {

        ValueA,

        ValueB

    }

    [TextFixture]

    public class EnumBinderTests

    {

        private IFilteredModelBinder binder;

        [SetUp]

        public void SetUp()

        {

            binder = new EnumBinder<TestEnum>(TestEnum.ValueB)

        }

        [Test]

        public void IsMatchShouldReturnTrueIfTypeIsSameAsGenericType()

        {

            // Act

            bool isMatch = binder.IsMatch(typeof(TestEnum));

            // Assert

            Assert.That(isMatch, Is.True);

        }

        [Test]

        public void IsMatchShouldReturnFalseIfTypeIsNotSameAsGenericType()

        {

            // Act

            bool isMatch = binder.IsMatch(typeof(string));

            // Assert

            Assert.That(isMatch, Is.False);

        }

        [Test]

        public void BindModelShouldReturnEnumValueForWhichValueAsStringIsPosted()

        {

            // Arrange

            ControllerContext controllerContext = GetControllerContext();

            ModelBindingContext bindingContext = GetModelBindingContext(new ValueProviderResult(null, “ValueA”, null));

            // ACT

            BindResult bindResult = binder.BindModel(controllerContext, bindingContext);

            // Assert

            Assert.That(bindResult.Value, Is.EqualTo(TestEnum.ValueA));

        }

        [Test]

        public void BindModelShouldReturnDefaultValueIfNoValueIsPosted()

        {

            // Arrange

            ControllerContext controllerContext = GetControllerContext();

            ModelBindingContext bindingContext = GetModelBindingContext(null);

            // Act

            BindResult bindResult = binder.BindModle(controllerContext, bindingContext);

            // Assert

            Assert.That(bindResult.Value, Is.EqualTo(TestEnum.ValueB));

        }

        [Test]

        public void BindModelShouldReturnDefaultValueIfUnknowValueIsPosted()

        {

            // Arrange

            ControllerContext controllerContext = GetControllerContext();

            ModelBindingContext bindingContext = GetModelBindingContext(new ValueProviderResult(ull, “Unknow”, null));

            // Act

            BindResult bindResult = binder.BindModel(controllerContext, bindingContext);

            // Assert

            Assert.That(bindResult.Value, Is.EqualTo(TestEnum.ValueB));

        }

        [Test]

        public void BindModelShouldReturnDefaultValueIfDefaultValueAsStringIsPosted()

        {

            // Arrange

            ControllerContext controllerContext = GetControllerContext();

            ModelBindingContext bindingContext = GetModelBindingContext(new ValueProviderResult(null, “ValueB”, null));

            // Act

            BindResult bindResult = binder.BindModel(controllerContext, bindingContext);

            // Assert

            Assert.That(bindResult.Value, Is.EqualTo(TestEnum.ValueB));

        }

        private static ControllerContext GetControllerContext()

        {

            return new ControllerContext {

    HttpContext = MockRepository.GenerateMock<HttpContextBase>()};

        }

        private static ModelBindingContext GetModelBindingContext(ValueProviderResult valueProviderResult)

        {

            ValueProviderDictionary dictionary = new ValueProviderDictionary(null)

            { {“enum”, valueProviderResult} };

            return new ModelBindingContext { ModelName = “enum”, ValueProvider = dictionary };

        }

    }

     

    [Maintainable MVC Series: Routing]

    [Maintainable MVC Series: Providers]

    [Maintainable MVC Series: Attributes]

    [Maintainable MVC Series: Handling sessions]

    [Maintainable MVC Series: Passing error and success message with TempData]

    [Maintainable MVC Series: Testable and reusable Javascript]

  • 相关阅读:
    自考新教材-p145_5
    自考新教材-p144_4
    自考新教材-p144_3
    自考新教材-p143_2
    自考新教材-p142_3(1)
    【SQL server】安装和配置
    【,net】发布网站问题
    【LR】关于宽带与比特之间的关系
    【LR】录制测试脚本中的基本菜单
    【LR】安装LR11后遇到的问题
  • 原文地址:https://www.cnblogs.com/zhaorui/p/20100329_Maintainable_MVC.html
Copyright © 2011-2022 走看看