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

11,734 views

Published on

В докладе рассматривается использование популярных фреймворков в разработке ASP.NET MVC приложения, как сделать его наиболее гибким. Будет затронута тема минимизации дублирования и повторное использование кода, применение методов метапрограммирования отображений; уменьшение логики в контроллерах; применение принципов SOLID и GRASP для разработки доменной модели приложения.

По материалам конференции .NET разработчиков http://www.dotnetconf.ru/Materialy/Asp_net_mvc_kak_postroit_gibkoe_web_prilozenie

Published in: Technology
1 Comment
7 Likes
Statistics
Notes
  • Угу да...
    Это хорошая презентация!!!
       Reply 
    Are you sure you want to  Yes  No
    Your message goes here
No Downloads
Views
Total views
11,734
On SlideShare
0
From Embeds
0
Number of Embeds
4,435
Actions
Shares
0
Downloads
0
Comments
1
Likes
7
Embeds 0
No embeds

No notes for slide

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

  1. 1. Вторая конференция .NET разработчиков<br />Как создать по-настоящему гибкое ASP.NET MVC приложение<br />Александр Зайцев<br />IndyCode<br />twitter.com/hazzik<br />
  2. 2. Давайте познакомимся?!<br />
  3. 3. О чем эта презентация?<br /><ul><li>Как избежать дублирования кода на всех уровнях MVC
  4. 4. Как эффектино повторно использовать компоненты приложений
  5. 5. Как протестировать все, ну или почти все</li></li></ul><li>Что мы используем?<br />Web framework:<br />ASP.NET MVChttp://www.asp.net/mvc <br />MvcExtensionshttp://mvcextensions.codeplex.com/<br />ORM:<br />NHibernatehttp://nhforge.org/<br />FluentNHibernatehttp://fluentnhibernate.org/<br />IoC:<br />Castle.Windsorhttp://castleproject.org/<br />Object-object mapper:<br />AutoMapperhttp://automapper.codeplex.com/<br />
  6. 6. Чем мы это тестируем?<br />TDD:<br />xUnit.nethttp://xunit.codeplex.com/<br />Moqhttp://code.google.com/p/moq/<br />Acceptance testing:<br />SpecFlowhttp://www.specflow.org/<br />Seleniumhttp://seleniumhq.org/<br />Autoithttp://www.autoitscript.com/<br />
  7. 7. У вас есть проблемы?<br />
  8. 8. Давайте их решать!?<br />
  9. 9. MVC<br />Model<br />Controller<br />View<br />
  10. 10. View<br />
  11. 11. Проблема первая:дублирование кода<br />
  12. 12. Проблема<br />@using (Html.BeginForm()) {<br />@Html.ValidationSummary(true, "Попытка входа неудачна.")<br /><divclass="editor-label">@Html.LabelFor(m => m.UserName)</div><br /><divclass="editor-field"><br />@Html.TextBoxFor(m => m.UserName)<br />@Html.ValidationMessageFor(m => m.UserName)<br /></div><br /><divclass="editor-label">@Html.LabelFor(m => m.Password)</div><br /><divclass="editor-field"><br />@Html.PasswordFor(m => m.Password)<br />@Html.ValidationMessageFor(m => m.Password)<br /></div><br /><divclass="editor-label"><br />@Html.CheckBoxFor(m => m.RememberMe)<br />@Html.LabelFor(m => m.RememberMe)<br /></div><br /><p><inputtype="submit"value="Впустите!"/></p><br />}<br />
  13. 13. <ul><li>Для каждого типа поля необходимо использовать свой хелпер. А если подходящего нет?
  14. 14. Необходимо заботиться о вспомогательной верстке
  15. 15. Много кода и дублирование;)</li></li></ul><li>Решение:Использовать мощь метаданных<br />@using (Html.BeginForm()) {<br />@Html.ValidationSummary(true, "Попытка входа неудачна.")<br />@Html.EditorForModel()<br /><p><inputtype="submit"value="Впустите!"/></p><br />}<br />
  16. 16. <ul><li>Не нужно думать какой тип поля используется
  17. 17. Вспомогательная верстка из коробки
  18. 18. Стандартный вид
  19. 19. Расширяемо
  20. 20. Минусов нет</li></li></ul><li>Когда использовать?<br /><ul><li>Много форм.
  21. 21. Все формы должны выглядеть стандартно
  22. 22. Много полей на форме.</li></li></ul><li>Проблема вторая:сложность метаданных<br />
  23. 23. Проблема<br />publicclassRegister<br />{<br /> [Required]<br /> [Display(Name = "Имя пользователя")]<br />publicstringUserName { get; set; }<br /> <br /> [Required]<br /> [DataType(DataType.EmailAddress)]<br /> [Display(Name = "Адрес электронной почты")]<br />publicstringEmail { get; set; }<br /> <br /> [Required]<br /> [ValidatePasswordLength]<br />[ValidatePasswordComplexy]<br /> [DataType(DataType.Password)]<br /> [Display(Name = "Пароль")]<br />publicstringPassword { get; set; }<br /> <br /> [DataType(DataType.Password)]<br /> [Display(Name = "Пароль еще раз")]<br /> [Compare("Password", ErrorMessage = "Пароль и подтверждение пароля должны совпадать")]<br />publicstringConfirmPassword { get; set; }<br />}<br />
  24. 24. <ul><li>Громоздко
  25. 25. Не расширяемо
  26. 26. Не поддерживаемо
  27. 27. Не тестируемо</li></li></ul><li>Решение:Использовать MvcExtensions<br />publicclassRegisterMetadata : ModelMetadataConfiguration<Register><br />{<br />publicRegisterMetadata()<br /> {<br />Configure(x => x.Login)<br />.DisplayName("Имя пользователя")<br /> .Required("Необходимо указать имя пользователя");<br /> <br />Configure(x => x.Email)<br /> .DisplayName("Адрес электронной почты")<br /> .Required("Необходимоуказать адрес электронной почты")<br />.AsEmail();<br /> <br />Configure(x => x.Password)<br />.DisplayName("Пароль")<br /> .Required("Необходимо указать пароль")<br /> .MinimumLength(6, "Длина пароля должна быть не меньше 6 символов")<br />.AsPassword();<br /> <br />Configure(x => x.ConfirmPassword)<br />.DisplayName("Пароль еще раз")<br /> .Required("Необходимо указать подтверждение пароля")<br /> .AsPassword()<br /> .Compare("Password", "Пароль и подтверждение пароля должны совпадать");<br /> }<br />}<br />
  28. 28. Примерырасширения:Календарики справочник<br />publicclassSetResponsibleMetadata : ModelMetadataConfiguration<SetResponsible><br />{<br />publicSetResponsibleMetadata()<br /> {<br />Configure(x => x.Deputy)<br />.AsReference("Пользователи", <br />x => x.Action("ListAllWithoutMe", "AccountsClassifier"))<br />.DisplayName("Выберите ответсвенного")<br /> .Required("Не выбран ответсвенный");<br /> <br />Configure(x => x.FromTime)<br /> .DisplayName("В период с")<br />.Required("Не выбран период времени")<br />.AsDatePicker(FutureOrPast.Future);<br /> }<br />}<br />
  29. 29. Как это работает?<br />publicstaticValueTypeMetadataItemBuilder<DateTime> AsDatePicker(<br />thisValueTypeMetadataItemBuilder<DateTime> itemBuilder, FutureOrPastfutureOrPast)<br />{<br />itemBuilder.Template("DateTime");<br /> <br />var setting = itemBuilder.Item.GetAdditionalSettingOrNew<DateTimeSetting>();<br />setting.FutureOrPast = futureOrPast;<br />returnitemBuilder;<br />}<br />publicclassDateTimeSetting : IModelMetadataAdditionalSetting<br />{ <br />publicFutureOrPast? FutureOrPast; <br />}<br /> <br />publicenumFutureOrPast<br />{<br />Future,<br />Past<br />}<br />
  30. 30. <ul><li>Расширяемо: возможности не ограничены
  31. 31. Легко поддерживать
  32. 32. Это можно тестировать!
  33. 33. Минусов нет
  34. 34. Использовать всегда.</li></li></ul><li>Проблема третья:binding<br />
  35. 35. Collections binding<br /><ul><li>Collection.cshtml</li></ul>Добавить скрытое поле для индекса элементов<br />
  36. 36. Пример<br />@model IEnumerable<br />@{<br />if (Model != null){<br />stringoldPrefix = ViewData.TemplateInfo.HtmlFieldPrefix;<br />var random = newRandom();<br />ViewData.TemplateInfo.HtmlFieldPrefix = String.Empty;<br /> <br />foreach (objectiteminModel){<br />int index = random.Next();<br />@Html.Hidden(string.Format("{0}.Index", oldPrefix), index)<br />stringfieldName = string.Format("{0}[{1}]", oldPrefix, index);<br />@Html.EditorFor(_ => item, null, fieldName);<br />}<br /> <br />ViewData.TemplateInfo.HtmlFieldPrefix = oldPrefix;<br /> }<br />}<br />
  37. 37. Complex forms<br /><ul><li>Object.cshtml</li></ul>Убрать ограничение вложенности для сложных объектов<br />
  38. 38. Проблема четвертая:организация JavaScript<br />
  39. 39. Решение<br /><ul><li>Вынести весь JavaScript из view в отдельные .jsфайлы
  40. 40. Использовать паттерн модуль
  41. 41. Компоновать все .jsфайлы в один и минимизировать его</li></li></ul><li>Пример<br /><scripttype="text/javascript"><br />App.views.products.FindProducts.init();<br /></script><br />App.namespace('App.views.products');<br />App.views.products.FindProducts = (function () {<br />//private scope<br />return {<br />init: function () {<br />//public API<br /> }<br /> };<br />})(jQuery);<br />
  42. 42. Controller<br />
  43. 43. Толстые контроллеры<br />
  44. 44. Обработка форм<br />
  45. 45. Обработка форм<br /><ul><li>GET
  46. 46. POST
  47. 47. Redirect
  48. 48. GET</li></ul>А если ошибка?<br />
  49. 49. Обработка форм<br />[HttpPost]<br />publicActionResultLogOn(LogOn form, stringreturnUrl)<br />{<br />if (ModelState.IsValid)<br /> {<br />if (MembershipService.ValidateUser(form.UserName, form.Password))<br /> {<br />FormsService.SignIn(form.UserName, form.RememberMe);<br />if (Url.IsLocalUrl(returnUrl))<br /> {<br />returnRedirect(returnUrl);<br /> }<br />returnRedirectToAction("Index", "Home");<br /> }<br />ModelState.AddModelError("", "Имя пользователя или пароль не верны!");<br /> }<br /> <br />// If we got this far, something failed, redisplay form<br />returnView(form);<br />}<br />
  50. 50. Просто redirect!<br />[HttpPost]<br />publicActionResultLogOn(LogOnModel form, stringreturnUrl)<br />{<br />if (ModelState.IsValid)<br /> {<br />if (MembershipService.ValidateUser(form.UserName, form.Password))<br /> {<br />FormsService.SignIn(form.UserName, form.RememberMe);<br />if (Url.IsLocalUrl(returnUrl))<br /> {<br />returnRedirect(returnUrl);<br /> }<br />returnRedirectToAction("Index", "Home");<br /> }<br />ModelState.AddModelError("", "The user name or password provided is incorrect.");<br /> }<br />// If we got this far, something failed, save model state and redirect<br />TempData[modelStateKey] = ModelState;<br />returnRedirectToAction("LogOn");<br />}<br />
  51. 51. Как это работает?<br />protectedoverridevoidOnActionExecuted(ActionExecutedContextfilterContext)<br />{<br />if (TempData[modelStateKey] != null && <br />ModelState.Equals(TempData[modelStateKey]) == false)<br />ModelState.Merge((ModelStateDictionary) TempData[modelStateKey]);<br /> <br />base.OnActionExecuted(filterContext);<br />}<br />
  52. 52. Что нам это даст?<br /><ul><li>Не нужно думать какие данные и как их отобразить на форме
  53. 53. Работает даже со сложными формами
  54. 54. Использовать всегда!</li></li></ul><li>А дальше?<br />[HttpPost]<br />publicActionResultLogOn(LogOn form, stringreturnUrl)<br />{<br />if (ModelState.IsValid)<br /> {<br />if (MembershipService.ValidateUser(form.UserName, form.Password))<br />{<br />FormsService.SignIn(form.UserName, form.RememberMe);<br />if (Url.IsLocalUrl(returnUrl))<br />{<br />returnRedirect(returnUrl);<br />}<br />returnRedirectToAction("Index", "Home");<br />}<br />ModelState.AddModelError("", "The user name or password provided is incorrect.");<br /> }<br /> <br />// If we got this far, something failed, redisplay form<br />returnRedirectToAction("LogOn");<br />}<br />
  55. 55. CQS/CQRS<br /><ul><li>Queries: Return a result and do not change the observable state of the system (are free of side effects).
  56. 56. Commands: Change the state of a system but do not return a value.</li></li></ul><li>Используйте команды!<br />[HttpPost]<br />publicActionResultLogOn(LogOn form, stringreturnUrl)<br />{<br />returnHandle(form,<br />successResult: GetRedirectToUrlOrHome(returnUrl),<br />failResult: RedirectToAction("LogOn"));<br />}<br />
  57. 57. Как это работает?<br />publicIDependencyResolverDependencyResolver { get; set; }<br /> <br />privateActionResultHandle<TCommand>(TCommand command, <br />ActionResultsuccessResult, ActionResultfailResult) whereTCommand : ICommand<br />{<br />if (ModelState.IsValid)<br /> {<br />try<br /> {<br />DependencyResolver.GetService<ICommandHandler<TCommand>>().Handle(command);<br />returnsuccessResult;<br /> }<br />catch (Exception e)<br /> {<br />ModelState.AddModelError("", e);<br /> }<br /> }<br />TempData[modelStateKey] = ModelState;<br />returnfailResult;<br />}<br />
  58. 58. Что нам это дает?<br />+<br />Легко тестировать<br />Не нужно тестировать контроллеры<br />Устранение дублирования<br />Повторное использование!<br />-<br />Логика на исключениях<br />Неявная связь с обработчиком<br />
  59. 59. Отображение данных<br />
  60. 60. Обработка форм<br />publicActionResultIndex()<br />{<br />IEnumerable<DepartmentSummaryModel> departments = DepartmentRepository<br />.FindAll(d => d.Company.Id==CurrentCompany.Id)<br /> .Select(d => newDepartmentSummaryModel<br /> {<br />Id = d.Id,<br />Name = d.Name,<br />NumberOfEmployees = d.Employments.Count(e => !e.Employee.Archived)<br /> })<br /> .OrderBy(d => d.Name)<br /> .ToList();<br /> <br />returnView(newListModel<DepartmentSummaryModel>(departments));<br />}<br />
  61. 61. Используйте запросы!<br />[HttpGet]<br />publicActionResultIndex()<br />{<br />IEnumerable<DepartmentSummaryModel> model = Query.For<DepartmentSummaryModel>()<br /> .With(newCurrentCompany())<br /> .MapTo<ListModel<DepartmentSummaryModel>>();<br /> <br />returnView(model);<br />}<br />
  62. 62. Как это работает?<br />public IQueryBuilderQuery { get; set; }<br />publicclassQueryBuilder : IQueryBuilder<br />{<br />publicIQueryFor<TResult> For<TResult>()<br />{<br />returnnewQueryFor<TResult>(dependencyResolver);<br />}<br /> <br />privateclassQueryFor<TResult> : IQueryFor<TResult><br />{<br />publicTResultWith<TCriterion>(TCriterion criterion) <br />whereTCriterion : ICriterion<br />{<br />returndependencyResolver<br />.GetService<IQuery<TCriterion, TResult>>()<br />.Ask(criterion);<br /> } <br /> }<br />}<br />
  63. 63. AutoMapper!<br />publicstaticclassMapperExtensions<br />{<br />publicstaticTResultMapTo<TResult>(thisobject @object)<br /> {<br />return (TResult) Mapper.Map(@object, <br /> @object.GetType(), <br />typeof (TResult));<br />}<br />}<br />
  64. 64. Что нам это дает?<br />+<br />Нет зависимости от источника данных<br />Легко тестировать<br />Повторное использование!<br />-<br />Неявная связь с запросом<br />
  65. 65. Итог<br /><ul><li>Повторное использование запросов и команд
  66. 66. Переносимо
  67. 67. Тестируемо
  68. 68. Просто</li></li></ul><li>Model<br />
  69. 69. Complex model binding<br />
  70. 70. Проблемка<br />publicclassNewProductHandler : ICommandHandler<NewProduct><br />{<br />publicvoidHandle(NewProductcommand)<br /> {<br />var product = newProduct();<br /> <br />var category = session.Get<Category>(command.CategoryId);<br /> <br />product.Category = category;<br />product.Name = command.Name;<br /> <br /> <br />uow.Save(product);<br />}<br />}<br />
  71. 71. Решение:EntityModelBinder<br />publicclassEntityModelBinder : IModelBinder<br />{<br />publicobjectBindModel(ControllerContextcontrollerContext,<br />ModelBindingContextbindingContext)<br /> {<br />ValueProviderResult value = bindingContext.ValueProvider<br /> .GetValue(bindingContext.ModelName);<br /> <br />if (value == null)<br />returnnull;<br /> <br />if (string.IsNullOrEmpty(value.AttemptedValue))<br />returnnull;<br /> <br />intentityId = int.Parse(value.AttemptedValue);<br />IRepository repository = GetRepository(bindingContext);<br />returnrepository.GetById(entityId);<br /> }<br />}<br /> <br /> <br />
  72. 72. Пример<br />publicclassNewProductHandler : ICommandHandler<NewProduct><br />{<br />publicvoidHandle(NewProductcommand)<br /> {<br />var product = newProduct();<br /> <br />product.Category = command.Category;<br />product.Name = command.Name;<br /> <br />uow.Save(product);<br />}<br />}<br />
  73. 73. Итог<br /><ul><li>Инфраструктура выполнит грязную работу за вас.
  74. 74. Больше сосредоточенности на задаче</li></li></ul><li>Ссылки<br /><ul><li>Steven Sanderson</li></ul>Pro ASP.NET MVC 3<br />http://blog.stevensanderson.com/<br /><ul><li>Jeffrey Palermo, JimmyBogard</li></ul>ASP.NET MVC 2 in Action<br />http://jeffreypalermo.com/<br />http://lostechies.com/jimmybogard/<br /><ul><li>KaziManzur Rashid</li></ul>http://kazimanzurrashid.com/<br />
  75. 75. Спасибо за внимание<br />Александр Зайцев<br />IndyCode<br />hazzik@indycode.ru<br />twitter.com/hazzik<br />
  76. 76. PS<br /><ul><li>Возьмите себя в рамки
  77. 77. Удивляйтесь
  78. 78. Экспериментируйте</li>

×