Продолжаю серию про похудание контроллеров. Ссылки на предыдущие части:
- Asp.Net Mvc. Худеем контроллеры. Обработка форм, ч. 1
- Asp.Net Mvc. Худеем контроллеры. Обработка форм, ч. 2
Как часто вам приходится писать подобный код?
public ActionResult Details( int feedbackId )
{
var feedback = this.DbSession.Get<Feedback>( feedbackId );
return View( feedback );
}
Лично у меня очень много вьюшек, которым на вход подается Id сущности, и первой строкой идет загрузка этой сущности из базы. И писать постоянно эту строчка задалбывает. Что можно сделать?
У MVC-шечки существует очень клевый механизм ModelBindnig'а, который позволяет преобразовывать по нашему усмотрению входные данные экшнов. Изучаем:
public interface IModelBinder
{
object BindModel( ControllerContext controllerContext, ModelBindingContext bindingContext );
}
Если мы хотим создать свой байндер, нам надо реализовать этот интерфейс - единственный метод, в который передается информация о текущем запросе и о параметре, который сейчас байндится. Реализуем:
public class EntityModelBinder : IModelBinder
{
public ISession DbSession { get; set; }
public object BindModel( ControllerContext controllerContext, ModelBindingContext bindingContext )
{
ValueProviderResult value = FindValue( bindingContext);
if ( value == null )
return null;
int id;
if( !int.TryParse( value.AttemptedValue, out id ) )
return null;
//Хибернейт позволяет достать объект как с помощью дженериковского вызова, так и передачей типа первым параметром
return this.DbSession.Get( bindingContext.ModelType, id );
}
public static ValueProviderResult FindValue( ModelBindingContext bindingContext )
{
//Список имен, которые мы будем пробовать, где ModelName - имя параметра у экшна, а ModelType - его тип
//Если у нас параметр "Feedback item", то имена будут "itemid", "feedbackid", "item", "feedback" и просто "id"
//последний может обернуться проблемой, если на вход подается две сущности, и одна из них может быть пустой
var names = new[]
{
bindingContext.ModelName + "id",
bindingContext.ModelType.Name + "id",
bindingContext.ModelName,
bindingContext.ModelType.Name,
"id",
};
//почему такой странный способ прочитать значение? Почему просто не обратиться к Request'у?
//Потому что не все данные берутся из запроса, часть - значения из маршрутизация по-умолчанию,
//часть может быть значениями, которые мы передале при вызове ChildAction
return names.Select( name => bindingContext.ValueProvider.GetValue( name ) )
.FirstOrDefault( value => value != null );
}
}
Осталось зарегистрировать наш байндер при старте приложения:
ModelBinders.Binders.Add( typeof( Feedback ), new EntityModelBinder() );
Тут первый сюрприз - байндеры регистрируются привязкой к определенному типу агрумента. Не весело. Что можно сделать? Например, пройтись рефлексией по всей сборке, найти все типы-сущности и зарегистрировать так каждый. И это даже сработает. Только меня корёжит от такого решения. :)
Тут на помощь приходит это чудо:
public interface IModelBinderProvider
{
IModelBinder GetBinder( Type modelType );
}
Я вообще практически поклоняюсь разработчикам Asp.Net Mvc - это очень гибкий и очень продуманный фреймворк, который практически никак не ограничивает меня в возможностях, и при этом предлагает тонны точек расширениия и столько же вспомогательных утилит. Прямо как NHibernate, только MVC. :) Если бы не несколько бесячих багов, которые не исправляются с доисторических времен, и какое-то непонятное направление развития MVC в сторону модных, но бесполезных фич (WebApi, AsyncControllers и прочее), то я бы убрал слово "пракически" из предыдущего предложения. Но первые три версии, особенно третья, были просто прорывом на прорыве на фоне WebForms.
К теме - IModelBinderProvider - это возможность встроится в место, где MVC выбирает ModelBinder для очередного параметра. Делаем:
public class EntityModelBinderProvider : IModelBinderProvider
{
public IModelBindersFactory Factory { get; set; }
public IModelBinder GetBinder( Type modelType )
{
if( !typeof( Entity ).IsAssignableFrom( modelType ) )
return null;
return this.Factory.MakeEntityModelBinder();
}
}
Лирическое отсупление номер 2 - вместо последней строчки можно было бы написать DependencyResolver.Current.GetService<EntityModelBinder>, т. к. это инфраструктурный код, но есть очень элегантный способ убрать зависимость от контейнера из кода, в котором нужно инстанциировать новые объекты через контейнер в момент выполнения. В случае с WindsorContainer-ом нужно только объявить интерфейс IModelBindersFactory и зарегистрировать его в контейнере особым образом. Реализацию он сделаем сам:
public interface IModelBindersFactory
{
EntityModelBinder MakeEntityModelBinder();
}
Component.For<IModelBindersFactory>().AsFactory().LifeStyle.Singleton
Теперь надо сказать MVC, что у нас появился свой провайдер. Это можно было бы сделать через ModelBinderProviders.BinderProviders, но, снова реверанс в сторону MVC - в третьей версии они добавили DependencyResolver. И, хотя, примеров его использования можно найти кучу на каждом шагу, нигде не заостряется внимания на таком факте: DependencyResolver - это не просто более элегантный способ создавать контроллеры через контейнер. Это, что самое важное, - механизм, который встроен глубоко и во все слои самого MVC. Что это означает? Что практически везде, где MVC инстанциирует свои компоненты, она всегда пытается попросить эти компоненты у DependencyResolver'а. И, только если последний вернул null, только тогда она откатывается в поведению MVC 2 - использованию Default* (DefaultControllerFactory, DefaultModelBinder и пр.). А это означает, что записи вида ModelBinderProviders.BinderProviders.Add или GlobalFilters.Filter.Add, ControllerBuilder.Current.SetControllerFactory давно устарели ( уже года 3 как ). В нашем случае достаточно зарегистрировать в контейнере наш провайдер, остальное MVC и контейнер сделают сами:
Component.For<IModelBinderProvider>().ImplementedBy<EntityModelBinderProvider>().LifeStyle.Singleton
Теперь мы можем писать экшны так:
public ActionResult Details( Feedback feedback )
{
return View( feedback );
}
Что интересно - если в качестве аргумента экшна будет какой-то сложный объект, типа:
public class FeedbackDeleteModel
{
public Feedback Feedback { get; set }
public string Reason { get; set }
}
То MVC-шечка и наш байндер справятся и с этим, благодаря тому, что байндер-по-умолчанию для сложных объектов, для каждого поля этого объекта вызывает практически весь тот же механизм, что и при байндинге отдельного параметра экшна (хотя точно помню, что без строчек bindingContext.ModelName и bindingContext.ModelType.Name это работать не будет, а вот почему - не помню - какая-то особенность в работе ComplexModelBinder).
Из наворотов можно разве что добавить байндинг параметров типа List<Feedback>, но это уже в качестве домашнего задания.
Итог
Хотя и не очень большое, но все такие приятное улучшение внешнего вида контроллеров. Да и тестировать попроще - не надо мочить доставание объекта из базы.
В следующий раз расскажу про то, как надо инкапсулировать работу с базой данных и объясню, почему Repositry - гадость.
Руки бы оторвал
ОтветитьУдалитьRepository - не гадость а лишь средство абстрагирования от конкретного способа хранения/управления данными. Например если планируется несколько провайдеров: MySQL, MS SQL Server...
ОтветитьУдалить