Вторая конференция .NET разработчиковКак создать по-настоящему гибкое ASP.NET MVC приложениеАлександр ЗайцевIndyCodetwitter.com/hazzik
Давайте познакомимся?!
О чем эта презентация?Как избежать дублирования кода на всех уровнях MVC
Как эффектино повторно использовать компоненты приложений
Как протестировать все, ну или почти всеЧто мы используем?Web framework:ASP.NET MVChttp://www.asp.net/mvc MvcExtensionshttp://mvcextensions.codeplex.com/ORM:NHibernatehttp://nhforge.org/FluentNHibernatehttp://fluentnhibernate.org/IoC:Castle.Windsorhttp://castleproject.org/Object-object mapper:AutoMapperhttp://automapper.codeplex.com/
Чем мы это тестируем?TDD:xUnit.nethttp://xunit.codeplex.com/Moqhttp://code.google.com/p/moq/Acceptance testing:SpecFlowhttp://www.specflow.org/Seleniumhttp://seleniumhq.org/Autoithttp://www.autoitscript.com/
У вас есть проблемы?
Давайте их решать!?
MVCModelControllerView
View
Проблема первая:дублирование кода
Проблема@using (Html.BeginForm()) {@Html.ValidationSummary(true, "Попытка входа неудачна.")<divclass="editor-label">@Html.LabelFor(m => m.UserName)</div><divclass="editor-field">@Html.TextBoxFor(m => m.UserName)@Html.ValidationMessageFor(m => m.UserName)</div><divclass="editor-label">@Html.LabelFor(m => m.Password)</div><divclass="editor-field">@Html.PasswordFor(m => m.Password)@Html.ValidationMessageFor(m => m.Password)</div><divclass="editor-label">@Html.CheckBoxFor(m => m.RememberMe)@Html.LabelFor(m => m.RememberMe)</div><p><inputtype="submit"value="Впустите!"/></p>}
Для каждого типа поля необходимо использовать свой хелпер. А если подходящего нет?
Необходимо заботиться о вспомогательной верстке
Много кода и дублирование;)Решение:Использовать мощь метаданных@using (Html.BeginForm()) {@Html.ValidationSummary(true, "Попытка входа неудачна.")@Html.EditorForModel()<p><inputtype="submit"value="Впустите!"/></p>}
Не нужно думать какой тип поля используется
Вспомогательная верстка из коробки
Стандартный вид
Расширяемо
Минусов нетКогда использовать?Много форм.
Все формы должны выглядеть стандартно
Много полей на форме.Проблема вторая:сложность метаданных
ПроблемаpublicclassRegister{ [Required] [Display(Name = "Имя пользователя")]publicstringUserName { get; set; }  [Required] [DataType(DataType.EmailAddress)] [Display(Name = "Адрес электронной почты")]publicstringEmail { get; set; }  [Required] [ValidatePasswordLength][ValidatePasswordComplexy] [DataType(DataType.Password)] [Display(Name = "Пароль")]publicstringPassword { get; set; }  [DataType(DataType.Password)] [Display(Name = "Пароль еще раз")] [Compare("Password", ErrorMessage = "Пароль и подтверждение пароля должны совпадать")]publicstringConfirmPassword { get; set; }}
Громоздко
Не расширяемо
Не поддерживаемо
Не тестируемоРешение:Использовать MvcExtensionspublicclassRegisterMetadata : ModelMetadataConfiguration<Register>{publicRegisterMetadata() {Configure(x => x.Login).DisplayName("Имя пользователя") .Required("Необходимо указать имя пользователя"); Configure(x => x.Email) .DisplayName("Адрес электронной почты") .Required("Необходимоуказать адрес электронной почты").AsEmail(); Configure(x => x.Password).DisplayName("Пароль") .Required("Необходимо указать пароль") .MinimumLength(6, "Длина пароля должна быть не меньше 6 символов").AsPassword(); Configure(x => x.ConfirmPassword).DisplayName("Пароль еще раз") .Required("Необходимо указать подтверждение пароля") .AsPassword() .Compare("Password", "Пароль и подтверждение пароля должны совпадать"); }}
Примерырасширения:Календарики справочникpublicclassSetResponsibleMetadata : ModelMetadataConfiguration<SetResponsible>{publicSetResponsibleMetadata() {Configure(x => x.Deputy).AsReference("Пользователи", x => x.Action("ListAllWithoutMe", "AccountsClassifier")).DisplayName("Выберите ответсвенного") .Required("Не выбран ответсвенный"); Configure(x => x.FromTime) .DisplayName("В период с").Required("Не выбран период времени").AsDatePicker(FutureOrPast.Future); }}
Как это работает?publicstaticValueTypeMetadataItemBuilder<DateTime> AsDatePicker(thisValueTypeMetadataItemBuilder<DateTime> itemBuilder, FutureOrPastfutureOrPast){itemBuilder.Template("DateTime"); var setting = itemBuilder.Item.GetAdditionalSettingOrNew<DateTimeSetting>();setting.FutureOrPast = futureOrPast;returnitemBuilder;}publicclassDateTimeSetting : IModelMetadataAdditionalSetting{ publicFutureOrPast? FutureOrPast; } publicenumFutureOrPast{Future,Past}
Расширяемо: возможности не ограничены
Легко поддерживать
Это можно тестировать!
Минусов нет
Использовать всегда.Проблема третья:binding
Collections bindingCollection.cshtmlДобавить скрытое поле для индекса элементов
Пример@model IEnumerable@{if (Model != null){stringoldPrefix = ViewData.TemplateInfo.HtmlFieldPrefix;var random = newRandom();ViewData.TemplateInfo.HtmlFieldPrefix = String.Empty; foreach (objectiteminModel){int index = random.Next();@Html.Hidden(string.Format("{0}.Index", oldPrefix), index)stringfieldName = string.Format("{0}[{1}]", oldPrefix, index);@Html.EditorFor(_ => item, null, fieldName);} ViewData.TemplateInfo.HtmlFieldPrefix = oldPrefix; }}
Complex formsObject.cshtmlУбрать ограничение вложенности для сложных объектов
Проблема четвертая:организация JavaScript
РешениеВынести весь JavaScript из view в отдельные .jsфайлы
Использовать паттерн модуль
Компоновать все .jsфайлы в один и минимизировать егоПример<scripttype="text/javascript">App.views.products.FindProducts.init();</script>App.namespace('App.views.products');App.views.products.FindProducts = (function () {//private scopereturn {init: function () {//public API } };})(jQuery);
Controller
Толстые контроллеры
Обработка форм
Обработка формGET
POST
Redirect
GETА если ошибка?
Обработка форм[HttpPost]publicActionResultLogOn(LogOn form, stringreturnUrl){if (ModelState.IsValid) {if (MembershipService.ValidateUser(form.UserName, form.Password)) {FormsService.SignIn(form.UserName, form.RememberMe);if (Url.IsLocalUrl(returnUrl)) {returnRedirect(returnUrl); }returnRedirectToAction("Index", "Home"); }ModelState.AddModelError("", "Имя пользователя или пароль не верны!"); } // If we got this far, something failed, redisplay formreturnView(form);}
Просто redirect![HttpPost]publicActionResultLogOn(LogOnModel form, stringreturnUrl){if (ModelState.IsValid) {if (MembershipService.ValidateUser(form.UserName, form.Password)) {FormsService.SignIn(form.UserName, form.RememberMe);if (Url.IsLocalUrl(returnUrl)) {returnRedirect(returnUrl); }returnRedirectToAction("Index", "Home"); }ModelState.AddModelError("", "The user name or password provided is incorrect."); }// If we got this far, something failed, save model state and redirectTempData[modelStateKey] = ModelState;returnRedirectToAction("LogOn");}
Как это работает?protectedoverridevoidOnActionExecuted(ActionExecutedContextfilterContext){if (TempData[modelStateKey] != null && ModelState.Equals(TempData[modelStateKey]) == false)ModelState.Merge((ModelStateDictionary) TempData[modelStateKey]); base.OnActionExecuted(filterContext);}
Что нам это даст?Не нужно думать какие данные и как их отобразить на форме
Работает даже со сложными формами
Использовать всегда!А дальше?[HttpPost]publicActionResultLogOn(LogOn form, stringreturnUrl){if (ModelState.IsValid) {if (MembershipService.ValidateUser(form.UserName, form.Password)){FormsService.SignIn(form.UserName, form.RememberMe);if (Url.IsLocalUrl(returnUrl)){returnRedirect(returnUrl);}returnRedirectToAction("Index", "Home");}ModelState.AddModelError("", "The user name or password provided is incorrect."); } // If we got this far, something failed, redisplay formreturnRedirectToAction("LogOn");}
CQS/CQRSQueries: Return a result and do not change the observable state of the system (are free of side effects).
Commands: Change the state of a system but do not return a value.Используйте команды![HttpPost]publicActionResultLogOn(LogOn form, stringreturnUrl){returnHandle(form,successResult: GetRedirectToUrlOrHome(returnUrl),failResult: RedirectToAction("LogOn"));}
Как это работает?publicIDependencyResolverDependencyResolver { get; set; } privateActionResultHandle<TCommand>(TCommand command, ActionResultsuccessResult, ActionResultfailResult) whereTCommand : ICommand{if (ModelState.IsValid) {try {DependencyResolver.GetService<ICommandHandler<TCommand>>().Handle(command);returnsuccessResult; }catch (Exception e) {ModelState.AddModelError("", e); } }TempData[modelStateKey] = ModelState;returnfailResult;}
Что нам это дает?+Легко тестироватьНе нужно тестировать контроллерыУстранение дублированияПовторное использование!-Логика на исключенияхНеявная связь с обработчиком

ASP.NET MVC - как построить по-настоящему гибкое веб-приложение