Получение данных в классе ASP.NET MVC ViewModel?
Для тех, кто создает ViewModels (для использования в типизированных представлениях) в ASP.NET MVC, предпочитаете ли вы получать данные из службы/репозитория из класса ViewModel или класса контроллера?
Например, мы начали с того, что ViewModels по существу были DTO и позволяли нашим контроллерам извлекать данные (грубо упрощенческий пример предполагает, что пользователь может изменять только имя сотрудника):
public class EmployeeViewModel
{
public String Name; //posted back
public int Num; //posted back
public IEnumerable<Dependent> Dependents; //static
public IEnumerable<Spouse> Spouses; //static
}
public class EmployeeController()
{
...
public ActionResult Employee(int empNum)
{
Models.EmployeeViewModel model = new Models.EmployeeViewModel();
model.Name = _empSvc.FetchEmployee(empNum).Name;
model.Num = empNum;
model.Dependents = _peopleSvc.FetchDependentsForView(empNum);
model.Spouses = _peopleSvc.FetchDependentsForView(empNum);
return View(model);
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Employee(Models.EmployeeViewModel model)
{
if (!_empSvc.ValidateAndSaveName(model.Num, model.Name))
{
model.Dependents = _peopleSvc.FetchDependentsForView(model.Num);
model.Spouses = _peopleSvc.FetchDependentsForView(model.Num);
return View(model);
}
this.RedirectToAction(c => c.Index());
}
}
Все это казалось прекрасным, пока мы не начали создавать большие виды (40+ полей) со многими выпадающими списками и т.д. Поскольку на экранах было бы действие GET и POST (с возвратом POST-представления, если была ошибка проверки), мы будем дублировать код и делать ViewModels больше, чем они, вероятно, должны быть.
Я думаю, что альтернативой было бы получение данных через Сервис в ViewModel. Меня беспокоит то, что у нас были бы некоторые данные, заполненные из ViewModel, а некоторые из Controller (например, в приведенном выше примере, Name будет заполнено из контроллера, поскольку оно является опубликованным значением, тогда как зависимые и супруги будут заполнены через некоторые тип функции GetStaticData() в ViewModel).
Мысли?
Ответы
Ответ 1
Я столкнулся с той же проблемой. Я начал создавать классы для каждого действия, когда код стал слишком большим для методов действий. Да, у вас будет некоторый поиск данных в классах, а некоторые - в методах контроллера. Альтернативой является получение всех данных в классах, но половина классов, которые вам не понадобятся, они будут созданы для обеспечения согласованности или будут получены все данные в методах контроллера, но опять же некоторые из этих методов будут быть слишком сложным и нуждаться в том, чтобы быть отвлеченным на классы... так что заберите свой яд. Я бы предпочел немного непоследовательность и имел правильное решение для этой работы.
Что касается ввода поведения в ViewModel, я этого не делаю, точка ViewModel должна быть тонким классом для установки и извлечения значений из представления.
Были случаи, когда я поместил методы преобразования в ViewModel. Например, мне нужно преобразовать ViewModel в соответствующий объект или мне нужно загрузить ViewModel с данными из Entity.
Чтобы ответить на ваш вопрос, я предпочитаю извлекать данные из с помощью методов контроллера/действий.
Как правило, с DropDowns я создаю выпадающий сервис. DropDowns - это те же данные, что и представления. С раскрывающимися списками в службе я могу использовать их в других представлениях и/или кэшировать их.
В зависимости от макета 40 полей могут создавать загроможденное представление. В зависимости от типа данных я бы попытался охватить многие поля в нескольких представлениях с помощью какого-либо интерфейса с вкладками или мастеров.
Ответ 2
Там больше, чем;-) Вы можете получить в модуле связывания или фильтра действий. Для второго варианта, проверьте блог Jimmy Bogard где-нибудь около здесь. Я лично делаю это в модельных вяжущих. Я использую ViewModel следующим образом: Мое пользовательское связывание объектов ASP.NET MVC: это хорошее решение?. Он обрабатывается моим пользовательским связующим устройством:
public object BindModel(ControllerContext c, BindingContext b)
{
var id = b.ValueProvider[b.ModelName]; // don't remember exact syntax
var repository = ServiceLocator.GetInstance(GetRepositoryType(b.ModelType));
var obj = repository.Get(id);
if (obj == null)
b.ModelState.AddModelError(b.ModelName, "Not found in database");
return obj;
}
public ActionResult Action(EntityViewModel<Order> order)
{
if (!ModelState.IsValid)
...;
}
Вы также можете увидеть пример привязки модели к доступу к репозиторию в Архитектура S # arp.
Что касается статических данных в моделях, я все еще изучаю подходы. Например, вы можете использовать свои модели просмотра для объектов вместо списков, а
открытый класс MyViewModel { public MyViewModel (порядок заказа, IEmployeesSvc _svc) { }
public IList<Employee> GetEmployeesList()
{
return _svc.GetEmployeesFor(order.Number);
}
}
Вы решаете, как вы вводите _svc в ViewModel, но в основном так же, как и для контроллера. Просто остерегайтесь того, что ViewModel также создается MVC с помощью конструктора без параметров, поэтому вы либо используете ServiceLocator, либо расширяете MVC для создания ViewModel - например, внутри своего настраиваемого связующего объекта. Или вы можете использовать подход Джимми Богарда с AutoMapper, который также поддерживает контейнеры IoC.
Общий подход здесь заключается в том, что всякий раз, когда я вижу повторяющийся код, я стараюсь его устранить. 100 действий контроллера, выполняющих поиск домена-viewmodel, а также поиск репозитория - это плохой случай. Единственное связующее устройство, делающее это в общем виде, является хорошим.
Ответ 3
Я бы не собирал данные из базы данных в ViewModel. ViewModel существует для обеспечения разделения проблем (между вашим представлением и вашей моделью). Привыкание логики сопротивления в этом случае проиграет цель.
К счастью, структура ASP.NET MVC дает нам больше точек интеграции, в частности ModelBinder.
У меня есть реализация генерирующей информации ModelBinder с уровня сервиса: -
http://www.iaingalloway.com/going-further-a-generic-servicelayer-modelbinder
Он не использует ViewModel, но это легко фиксируется. Это отнюдь не единственная реализация. Для реального проекта вы, вероятно, лучше с менее общим, более индивидуальным решением.
Если вы прилежны, ваши методы GET даже не должны знать, что сервисный уровень существует.
Решение, вероятно, выглядит примерно так: -
Метод действия контроллера: -
public ActionResult Details(MyTypeIndexViewModel model)
{
if( ModelState.IsValid )
{
return View(model);
}
else
{
// Handle the case where the ModelState is invalid
// usually because they've requested MyType/Details/x
// and there no matching MyType in the repository
// e.g. return RedirectToAction("Index")
}
}
ModelBinder: -
public object BindModel
(
ControllerContext controllerContext,
BindingContext bindingContext
)
{
// Get the Primary Key from the requestValueProvider.
// e.g. bindingContext.ValueProvider["id"]
int id = ...;
// Get an instance of your service layer via your
// favourite dependancy injection framework.
// Or grab the controller copy e.g.
// (controllerContext.Controller as MyController).Service
IMyTypeService service = ...;
MyType myType = service.GetMyTypeById(id)
if (myType == null)
{
// handle the case where the PK has no matching MyType in the repository
// e.g. bindingContext.ModelState.AddModelError(...)
}
MyTypeIndexViewModel model = new MyTypeIndexViewModel(myType);
// If you've got more repository calls to make
// (e.g. populating extra fields on the model)
// you can do that here.
return model;
}
ViewModel: -
public class MyTypeIndexViewModel
{
public MyTypeIndexViewModel(MyType source)
{
// Bind all the properties of the ViewModel in here, or better
// inherit from e.g. MyTypeViewModel, bind all the properties
// shared between views in there and chain up base(source)
}
}
Создайте свой сервисный уровень и зарегистрируйте свой ModelBinder как обычно.
Ответ 4
Здесь другое решение: http://www.lostechies.com/blogs/jimmy_bogard/archive/2009/06/29/how-we-do-mvc-view-models.aspx
Основные пункты:
- Отображение выполняется посредником - в этом случае это AutoMapper, но он может быть вашим собственным классом (хотя и больше для кода). Это удерживает как Domain, так и ViewModel сосредоточенными на логике домена/представления. Медиатор (mapper) будет содержать (в основном автоматическую) логику для сопоставления, включая внедренные службы.
- Сопоставление применяется автоматически, все, что вы делаете, это указать фильтру действия источника/назначения - очень чисто.
- (Кажется, это важно для вас). AutoMapper поддерживает вложенные сопоставления/типы, поэтому вы можете комбинировать ViewModel с несколькими независимыми моделями просмотра, чтобы ваш "экран DTO" не был грязным.
Как в этой модели:
public class WholeViewModel
{
public Part1ViewModel ModelPart1 { get; set; }
public Part2ViewModel ModelPart2 { get; set; }
}
вы повторно используете сопоставления для определенных частей вашего представления, и вы не пишете никакой новой строки кода, так как уже есть сопоставления для моделей с частичным представлением.
Если вы не хотите AutoMapper, у вас есть интерфейсы IViewModelMapper, а затем ваш контейнер IoC поможет вашему фильтру действий найти подходящий
container.Resolve(typeof(IViewModelMapper<>).MakeGenericType(mysourcetype, mydesttype))
и он также будет предоставлять любые необходимые внешние сервисы этому картографу (это возможно и с помощью AutoMapper). Но, конечно, AutoMapper может выполнять рекурсии и, во всяком случае, зачем писать дополнительный AutoMapper; -)
Ответ 5
Рассмотрите возможность передачи ваших услуг в пользовательский ViewModel на его конструкторе (ala Dependency Injection). Это удаляет код модели с вашего контроллера и позволяет сосредоточиться на управлении логическим потоком приложения. Пользовательские модели ViewModels - идеальное место для абстрагирования подготовки таких вещей, как SelectLists, от которых зависят ваши дротисты.
Множество кода в контроллере для таких вещей, как получение данных, не считается лучшей практикой. Основная ответственность контроллера заключается в "управлении" потоком приложения.
Ответ 6
Отправляя это позже... Баунти почти закончилась. Но...
Еще один просмотрщик - Automapper: http://www.codeplex.com/AutoMapper
И обзор о том, как его использовать: http://www.lostechies.com/blogs/jimmy_bogard/archive/2009/01/22/automapper-the-object-object-mapper.aspx
Мне очень нравится синтаксис.
// place this somewhere in your globals, or base controller constructor
Mapper.CreateMap<Employee, EmployeeViewModel>();
Теперь, в вашем контроллере, я бы использовал несколько режимов просмотра. Это обеспечивает DRY, позволяя вам повторно использовать эти режимы просмотра в другом месте приложения. Я бы не связывал их всех с 1 моделью. Я бы реорганизовал что-то вроде:
public class EmployeeController()
{
private IEmployeeService _empSvc;
private ISpouseService _peopleSvc;
public EmployeeController(
IEmployeeService empSvc, ISpouseService peopleSvc)
{
// D.I. hard at work! Auto-wiring up our services. :)
_empSvc = empSvc;
_peopleSvc = peopleSvc;
// setup all ViewModels here that the controller would use
Mapper.CreateMap<Employee, EmployeeViewModel>();
Mapper.CreateMap<Spouse, SpouseViewModel>();
}
public ActionResult Employee(int empNum)
{
// really should have some validation here that reaches into the domain
//
var employeeViewModel =
Mapper.Map<Employee, EmployeeViewModel>(
_empSvc.FetchEmployee(empNum)
);
var spouseViewModel =
Mapper.Map<Spouses, SpousesViewModel>(
_peopleSvc.FetchSpouseByEmployeeID(empNum)
);
employeeViewModel.SpouseViewModel = spouseViewModel;
return View(employeeViewModel);
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Employee(int id, FormCollection values)
{
try
{
// always post to an ID, which is the employeeID
var employee = _empSvc.FetchEmployee(id);
// and bind using the built-in UpdateModel helpers.
// this will throw an exception if someone is posting something
// they shouldn't be posting. :)
UpdateModel(employee);
// save employee here
this.RedirectToAction(c => c.Index());
}
catch
{
// check your domain model for any errors.
// check for any other type of exception.
// fail back to the employee screen
RedirectToAction(c => c.Employee(id));
}
}
}
Обычно я стараюсь держаться подальше от сохранения нескольких объектов в действии контроллера. Вместо этого я бы реорганизовал объект домена сотрудника на методы AddSpouse() и SaveSpouse(), которые возьмут объект Супруг. Эта концепция известна как AggregateRoots, контролирующая все зависимости от корня - это объект Employee(). Но, это только я.