在ASP.NET MVC ViewModel类中获取数据?

时间:2009-09-23 04:48:43

标签: asp.net-mvc

对于那些在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()函数类型。

思想?

6 个答案:

答案 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

主要观点:

  1. 映射由中介完成 - 在这种情况下,它是AutoMapper,但它可以是您自己的类(尽管更多是代码)。这使Domain和ViewModel都集中在域/表示逻辑上。中介(映射器)将包含(主要是自动)映射逻辑,包括注入的服务。
  2. 自动应用映射,您只需告诉操作过滤器源/目标类型 - 非常干净。
  3. (似乎对您很重要)AutoMapper支持嵌套映射/类型,因此您可以将ViewModel与多个独立视图模型相结合,这样您的“屏幕DTO”就不会混乱。
  4. 就像在这个模型中一样:

    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

关于如何使用它的概述: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个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()对象。但是,那只是我。