Upload
alexander-zaytsev
View
125
Download
2
Embed Size (px)
Citation preview
Как создать по-настоящему гибкое ASP.NET MVC приложение
Александр ЗайцевIndyCode
twitter.com/hazzik
Вторая конференция .NET разработчиков
Давайте познакомимся?!
О чем эта презентация?
• Как избежать дублирования кода на всех уровнях MVC
• Как эффектино повторно использовать компоненты приложений
• Как протестировать все, ну или почти все
Что мы используем?
Web framework:• ASP.NET MVC http://www.asp.net/mvc • MvcExtensions http://mvcextensions.codeplex.com/
ORM:• NHibernate http://nhforge.org/• FluentNHibernate http://fluentnhibernate.org/
IoC:• Castle.Windsor http://castleproject.org/
Object-object mapper:• AutoMapper http://automapper.codeplex.com/
Чем мы это тестируем?
TDD:• xUnit.net http://xunit.codeplex.com/• Moq http://code.google.com/p/moq/
Acceptance testing:• SpecFlow http://www.specflow.org/• Selenium http://seleniumhq.org/• Autoit http://www.autoitscript.com/
У вас есть проблемы?
Давайте их
решать!?
MVC
Model
ControllerView
View
Проблема первая:дублирование кода
Проблема@using (Html.BeginForm()) {
@Html.ValidationSummary(true, "Попытка входа неудачна.")
<div class="editor-label">@Html.LabelFor(m => m.UserName)</div>
<div class="editor-field">
@Html.TextBoxFor(m => m.UserName)
@Html.ValidationMessageFor(m => m.UserName)
</div>
<div class="editor-label">@Html.LabelFor(m => m.Password)</div>
<div class="editor-field">
@Html.PasswordFor(m => m.Password)
@Html.ValidationMessageFor(m => m.Password)
</div>
<div class="editor-label">
@Html.CheckBoxFor(m => m.RememberMe)
@Html.LabelFor(m => m.RememberMe)
</div>
<p><input type="submit" value="Впустите!" /></p>
}
• Для каждого типа поля необходимо использовать свой хелпер. А если подходящего нет?
• Необходимо заботиться о вспомогательной верстке
• Много кода и дублирование;)
Решение:Использовать мощь метаданных
@using (Html.BeginForm()) {
@Html.ValidationSummary(true, "Попытка входа неудачна.")
@Html.EditorForModel()
<p><input type="submit" value="Впустите!" /></p>
}
• Не нужно думать какой тип поля используется
• Вспомогательная верстка из коробки• Стандартный вид• Расширяемо• Минусов нет
Когда использовать?
• Много форм.• Все формы должны выглядеть
стандартно• Много полей на форме.
Проблема вторая:сложность метаданных
Проблемаpublic class Register
{
[Required]
[Display(Name = "Имя пользователя")]
public string UserName { get; set; }
[Required]
[DataType(DataType.EmailAddress)]
[Display(Name = "Адрес электронной почты")]
public string Email { get; set; }
[Required]
[ValidatePasswordLength]
[ValidatePasswordComplexy]
[DataType(DataType.Password)]
[Display(Name = "Пароль")]
public string Password { get; set; }
[DataType(DataType.Password)]
[Display(Name = "Пароль еще раз")]
[Compare("Password", ErrorMessage = "Пароль и подтверждение пароля должны совпадать")]
public string ConfirmPassword { get; set; }
}
• Громоздко• Не расширяемо• Не поддерживаемо• Не тестируемо
Решение:Использовать MvcExtensions
public class RegisterMetadata : ModelMetadataConfiguration<Register>
{
public RegisterMetadata()
{
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", "Пароль и подтверждение пароля должны совпадать");
}
}
Примеры расширения:Календарик и справочник
public class SetResponsibleMetadata : ModelMetadataConfiguration<SetResponsible>
{
public SetResponsibleMetadata()
{
Configure(x => x.Deputy)
.AsReference("Пользователи",
x => x.Action("ListAllWithoutMe", "AccountsClassifier"))
.DisplayName("Выберите ответсвенного")
.Required("Не выбран ответсвенный");
Configure(x => x.FromTime)
.DisplayName("В период с")
.Required("Не выбран период времени")
.AsDatePicker(FutureOrPast.Future);
}
}
Как это работает?public static ValueTypeMetadataItemBuilder<DateTime> AsDatePicker(
this ValueTypeMetadataItemBuilder<DateTime> itemBuilder, FutureOrPast futureOrPast)
{
itemBuilder.Template("DateTime");
var setting = itemBuilder.Item.GetAdditionalSettingOrNew<DateTimeSetting>();
setting.FutureOrPast = futureOrPast;
return itemBuilder;
}
public class DateTimeSetting : IModelMetadataAdditionalSetting
{
public FutureOrPast? FutureOrPast;
}
public enum FutureOrPast
{
Future,
Past
}
• Расширяемо: возможности не ограничены
• Легко поддерживать• Это можно тестировать!• Минусов нет• Использовать всегда.
Проблема третья:binding
Collections binding
• Collection.cshtml• Добавить скрытое поле для индекса
элементов
Пример@model IEnumerable
@{
if (Model != null) {
string oldPrefix = ViewData.TemplateInfo.HtmlFieldPrefix;
var random = new Random();
ViewData.TemplateInfo.HtmlFieldPrefix = String.Empty;
foreach (object item in Model) {
int index = random.Next();
@Html.Hidden(string.Format("{0}.Index", oldPrefix), index)
string fieldName = string.Format("{0}[{1}]", oldPrefix, index);
@Html.EditorFor(_ => item, null, fieldName);
}
ViewData.TemplateInfo.HtmlFieldPrefix = oldPrefix;
}
}
Complex forms
• Object.cshtml• Убрать ограничение вложенности для сложных
объектов
Пример@model IEnumerable
@{
if (Model != null) {
string oldPrefix = ViewData.TemplateInfo.HtmlFieldPrefix;
var random = new Random();
ViewData.TemplateInfo.HtmlFieldPrefix = String.Empty;
foreach (object item in Model) {
int index = random.Next();
@Html.Hidden(string.Format("{0}.Index", oldPrefix), index)
string fieldName = string.Format("{0}[{1}]", oldPrefix, index);
@Html.EditorFor(_ => item, null, fieldName);
}
ViewData.TemplateInfo.HtmlFieldPrefix = oldPrefix;
}
}
Проблема четвертая:
организация JavaScript
Решение
• Вынести весь JavaScript из view в отдельные .js файлы
• Использовать паттерн модуль• Компоновать все .js файлы в один и
минимизировать его
Пример<script type="text/javascript">
App.views.products.FindProducts.init();
</script>
App.namespace('App.views.products');App.views.products.FindProducts = (function () { //private scope return { init: function () { //public API } };})(jQuery);
Controller
Толстые контроллеры
Обработка форм
Обработка форм
• GET• POST• Redirect• GET
А если ошибка?
Обработка форм[HttpPost]
public ActionResult LogOn(LogOn form, string returnUrl)
{
if (ModelState.IsValid)
{
if (MembershipService.ValidateUser(form.UserName, form.Password))
{
FormsService.SignIn(form.UserName, form.RememberMe);
if (Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
return RedirectToAction("Index", "Home");
}
ModelState.AddModelError("", "Имя пользователя или пароль не верны!");
}
// If we got this far, something failed, redisplay form
return View(form);
}
Просто redirect![HttpPost]
public ActionResult LogOn(LogOnModel form, string returnUrl)
{
if (ModelState.IsValid)
{
if (MembershipService.ValidateUser(form.UserName, form.Password))
{
FormsService.SignIn(form.UserName, form.RememberMe);
if (Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
return RedirectToAction("Index", "Home");
}
ModelState.AddModelError("", "The user name or password provided is incorrect.");
}
// If we got this far, something failed, save model state and redirect
TempData[modelStateKey] = ModelState;
return RedirectToAction("LogOn");
}
Как это работает?
protected override void OnActionExecuted(ActionExecutedContext filterContext)
{
if (TempData[modelStateKey] != null &&
ModelState.Equals(TempData[modelStateKey]) == false)
ModelState.Merge((ModelStateDictionary) TempData[modelStateKey]);
base.OnActionExecuted(filterContext);
}
Что нам это даст?
• Не нужно думать какие данные и как их отобразить на форме
• Работает даже со сложными формами
• Использовать всегда!
А дальше?[HttpPost]
public ActionResult LogOn(LogOn form, string returnUrl)
{
if (ModelState.IsValid)
{
if (MembershipService.ValidateUser(form.UserName, form.Password))
{
FormsService.SignIn(form.UserName, form.RememberMe);
if (Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
return RedirectToAction("Index", "Home");
}
ModelState.AddModelError("", "The user name or password provided is incorrect.");
}
// If we got this far, something failed, redisplay form
return RedirectToAction("LogOn");
}
CQS/CQRS
• Queries: 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]
public ActionResult LogOn(LogOn form, string returnUrl)
{
return Handle(form,
successResult: GetRedirectToUrlOrHome(returnUrl),
failResult: RedirectToAction("LogOn"));
}
Как это работает?public IDependencyResolver DependencyResolver { get; set; }
private ActionResult Handle<TCommand>(TCommand command,
ActionResult successResult, ActionResult failResult) where TCommand : ICommand
{
if (ModelState.IsValid)
{
try
{
DependencyResolver.GetService<ICommandHandler<TCommand>>().Handle(command);
return successResult;
}
catch (Exception e)
{
ModelState.AddModelError("", e);
}
}
TempData[modelStateKey] = ModelState;
return failResult;
}
Что нам это дает?
+• Легко тестировать• Не нужно
тестировать контроллеры
• Устранение дублирования
• Повторное использование!
-
• Логика на исключениях
• Неявная связь с обработчиком
Отображение данных
Обработка формpublic ActionResult Index()
{
IEnumerable<DepartmentSummaryModel> departments = DepartmentRepository
.FindAll(d => d.Company.Id == CurrentCompany.Id)
.Select(d => new DepartmentSummaryModel
{
Id = d.Id,
Name = d.Name,
NumberOfEmployees = d.Employments.Count(e => !e.Employee.Archived)
})
.OrderBy(d => d.Name)
.ToList();
return View(new ListModel<DepartmentSummaryModel>(departments));
}
Используйте запросы!
[HttpGet]
public ActionResult Index()
{
IEnumerable<DepartmentSummaryModel> model = Query.For<DepartmentSummaryModel>()
.With(new CurrentCompany())
.MapTo<ListModel<DepartmentSummaryModel>>();
return View(model);
}
Как это работает?public IQueryBuilder Query { get; set; }
public class QueryBuilder : IQueryBuilder
{
public IQueryFor<TResult> For<TResult>()
{
return new QueryFor<TResult>(dependencyResolver);
}
private class QueryFor<TResult> : IQueryFor<TResult>
{
public TResult With<TCriterion>(TCriterion criterion)
where TCriterion : ICriterion
{
return dependencyResolver
.GetService<IQuery<TCriterion, TResult>>()
.Ask(criterion);
}
}
}
AutoMapper!
public static class MapperExtensions
{
public static TResult MapTo<TResult>(this object @object)
{
return (TResult) Mapper.Map(@object,
@object.GetType(),
typeof (TResult));
}
}
Что нам это дает?
+• Нет зависимости
от источника данных
• Легко тестировать
• Повторное использование!
-
• Неявная связь с запросом
Итог
• Повторное использование запросов и команд
• Переносимо• Тестируемо• Просто
Model
Complex model binding
Проблемкаpublic class NewProductHandler : ICommandHandler<NewProduct>
{
public void Handle(NewProduct command)
{
var product = new Product();
var category = session.Get<Category>(command.CategoryId);
product.Category = category;
product.Name = command.Name;
uow.Save(product);
}
}
Решение:EntityModelBinder
public class EntityModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
ValueProviderResult value = bindingContext.ValueProvider
.GetValue(bindingContext.ModelName);
if (value == null)
return null;
if (string.IsNullOrEmpty(value.AttemptedValue))
return null;
int entityId = int.Parse(value.AttemptedValue);
IRepository repository = GetRepository(bindingContext);
return repository.GetById(entityId);
}
}
Примерpublic class NewProductHandler :
ICommandHandler<NewProduct>
{
public void Handle(NewProduct command)
{
var product = new Product();
product.Category = command.Category;
product.Name = command.Name;
uow.Save(product);
}
}
Итог
• Инфраструктура выполнит грязную работу за вас.
• Больше сосредоточенности на задаче
Ссылки
• Steven Sanderson • Pro ASP.NET MVC 3• http://blog.stevensanderson.com/
• Jeffrey Palermo, Jimmy Bogard• ASP.NET MVC 2 in Action• http://jeffreypalermo.com/• http://lostechies.com/jimmybogard/
• Kazi Manzur Rashid• http://kazimanzurrashid.com/
PS
• Возьмите себя в рамки• Удивляйтесь• Экспериментируйте