对于那些在ASP.NET MVC中创建ViewModel(供类型化视图使用)的人,您更喜欢从ViewModel或控制器类中获取服务/存储库中的数据吗?
例如,我们首先让ViewModel基本上是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将返回视图),我们将复制代码并使ViewModel比应有的大。
我认为另一种方法是通过ViewModel中的服务获取数据。我担心的是,我们将从ViewModel填充一些数据,并从Controller中填充一些数据(例如,在上面的示例中,Name将从Controller填充,因为它是一个已发布的值,而Dependents和Spouses将通过一些填充ViewModel中的GetStaticData()函数类型。
思想?
答案 0 :(得分:7)
我遇到了同样的问题。当代码对于动作方法来说太大时,我开始为每个动作创建类。是的,您将在类中进行一些数据检索,在控制器方法中进行一些检索。另一种方法是在类中进行所有数据检索,但是你不会真正需要的一半类,它们是为了一致性而创建的,或者在控制器方法中有所有数据检索,但同样,其中一些方法将会过于复杂,需要被抽象出来......所以选择你的毒药。我宁愿有一点不一致,也没有合适的解决方案。
至于将行为放入ViewModel中,我不这样做,ViewModel的要点是成为从View中设置和提取值的瘦类。
有些情况下我将转换方法放在ViewModel中。例如,我需要将ViewModel转换为相应的实体,或者我需要使用来自实体的数据加载ViewModel。
要回答您的问题,我更喜欢在控制器/操作方法中检索数据。
通常使用DropDowns,我会创建一个下拉服务。 DropDowns往往是跨越视图的相同数据。通过服务中的下拉菜单,我可以在其他视图上使用它们和/或缓存它们。
根据布局,40多个字段可能会创建一个混乱的视图。根据数据的类型,我会尝试使用某种选项卡式或向导界面跨多个视图跨越那么多字段。
答案 1 :(得分:3)
还有更多;-)您可以获取模型绑定器或动作过滤器。对于第二个选项,请查看Jimmy Bogard在here附近的博客。我个人在模型粘合剂中这样做。我像这样使用ViewModel:My custom ASP.NET MVC entity binding: is it a good solution?。它由我的自定义模型绑定器处理:
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 Architecture中查看模型绑定器进行存储库访问的示例。
对于视图模型中的静态数据,我仍在探索方法。例如,您可以让视图模型记住实体而不是列表,
公共类MyViewModel { public MyViewModel(订单,IEmployeesSvc _svc) { }
public IList<Employee> GetEmployeesList()
{
return _svc.GetEmployeesFor(order.Number);
}
}
您决定如何将_svc注入ViewModel,但它与您对控制器的操作基本相同。请注意,ViewModel也是由MVC通过无参数构造函数创建的,因此您可以使用ServiceLocator或扩展MVC来创建ViewModel - 例如,在自定义模型绑定器中。或者你可以使用Jimmy Bogard的AutoMapper方法,它也支持IoC容器。
这里常见的方法是每当我看到重复代码时,我都希望消除它。执行domain-viewmodel编组和存储库查找的100个控制器操作是一个不好的情况。以通用方式执行的单一模型绑定器是一个很好的。
答案 2 :(得分:3)
我不会从ViewModel中的数据库中获取数据。 ViewModel的存在是为了促进关注点的分离(在View和模型之间)。在某种程度上纠缠于持久性逻辑的目的。
幸运的是,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's 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's 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;
}
视图模型: -
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。
答案 3 :(得分:1)
这是另一种解决方案:http://www.lostechies.com/blogs/jimmy_bogard/archive/2009/06/29/how-we-do-mvc-view-models.aspx
主要观点:
就像在这个模型中一样:
public class WholeViewModel
{
public Part1ViewModel ModelPart1 { get; set; }
public Part2ViewModel ModelPart2 { get; set; }
}
您重复使用View的特定部分的映射,并且不编写任何新的代码行,因为已经有部分视图模型的映射。
如果你不想要AutoMapper,你有IViewModelMapper接口,然后你的IoC容器将帮助你的动作过滤器找到合适的
container.Resolve(typeof(IViewModelMapper<>).MakeGenericType(mysourcetype, mydesttype))
它还将为该映射器提供任何所需的外部服务(这也适用于AutoMapper)。但是当然AutoMapper可以进行递归,无论如何,为什么要编写额外的AutoMapper; - )
答案 4 :(得分:0)
考虑将您的服务传递到其构造函数的自定义ViewModel(ala依赖注入)。这将从控制器中删除模型填充代码,并允许它专注于控制应用程序的逻辑流程。自定义ViewModel是一个理想的地方,用于抽象您的下拉列表所依赖的SelectLists等内容的准备工作。
控制器中有很多代码用于检索数据,这不是最佳做法。控制器的主要职责是“控制”应用程序的流程。
答案 5 :(得分:0)
迟到提交这件事......赏金快要结束了。但...
另一个要查看的映射器是Automapper:http://www.codeplex.com/AutoMapper
我真的很喜欢它的语法。
// place this somewhere in your globals, or base controller constructor
Mapper.CreateMap<Employee, EmployeeViewModel>();
现在,在你的控制器中,我会使用多个视图模型。这允许您在应用程序的其他位置重用这些视图模型,从而强制执行DRY。我不会将它们全部绑定到1个viewmodel。我会重构像:
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()方法,这将采用Spouse的对象。这个概念称为AggregateRoots,它控制来自根的所有依赖项 - 即Employee()对象。但是,那只是我。