【问题标题】:Fetching data within an ASP.NET MVC ViewModel class?在 ASP.NET MVC ViewModel 类中获取数据?
【发布时间】:2009-09-23 04:48:43
【问题描述】:

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

想法?

【问题讨论】:

  • IEnumerable?一夫多妻制模式是怎么回事? :-D

标签: asp.net-mvc


【解决方案1】:

我遇到了同样的问题。当代码对于动作方法来说太大时,我开始为每个动作创建类。是的,您将在类和控制器方法中进行一些数据检索。另一种方法是在类中进行所有数据检索,但是您实际上不需要的一半类是为了一致性而创建的,或者在控制器方法中进行所有数据检索,但同样,其中一些方法将太复杂了,需要被抽象成类......所以选择你的毒药。我宁愿有一点不一致,并有适合工作的解决方案。

至于将行为放入 ViewModel,我不知道,ViewModel 的重点是成为一个用于从 View 设置和提取值的瘦类。

在某些情况下,我将转换方法放在 ViewModel 中。例如,我需要将 ViewModel 转换为相应的实体,或者我需要使用来自实体的数据加载 ViewModel。

为了回答您的问题,我更喜欢从控制器/操作方法中检索数据。

我通常使用 DropDowns 创建一个下拉服务。 DropDowns 往往是跨视图的相同数据。通过服务中的下拉菜单,我可以在其他视图上使用它们和/或缓存它们。

根据布局,40 多个字段可能会创建混乱的视图。根据数据的类型,我会尝试使用某种选项卡式或向导界面跨越多个视图中的许多字段。

【讨论】:

  • 感谢您的反馈。这是针对安全客户端应用程序,而不是面向公众的应用程序,因此 40 个字段通常并不荒谬(尽管我们 100 多个计划视图中的大多数都接近 10-15 个字段)。当您说您创建一个下拉服务时,您的意思是与我在上面所做的相同,还是该服务填充一个 SelectList 对象并将其传递给 ViewModel?
  • 您的数据似乎依赖于当前员工。它看起来不错,只要它适合你。当我提到下拉服务时,我在考虑时区的下拉或国家的下拉。这种类型的数据不会经常变化,很容易被缓存。
  • 我们实际上也有这种类型的数据,我们对其进行缓存,但调用方式相同(缓存在服务层进行管理)。您将如何通过在 ViewModel 中调用此缓存信息以不同方式处理它?
  • 我想我不会。做事的方法有很多种,只要你的方法适合你,为什么要改变?
【解决方案2】:

还有更多 ;-) 您可以在模型绑定器或操作过滤器中获取。对于第二个选项,请查看 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(Order order, IEmployeesSvc _svc) { }

  public IList<Employee> GetEmployeesList()
  {
      return _svc.GetEmployeesFor(order.Number);
  }

}

您决定如何将 _svc 注入 ViewModel,但这与您为控制器所做的基本相同。请注意 ViewModel 也是由 MVC 通过无参数构造函数创建的,因此您可以使用 ServiceLocator 或扩展 MVC 来创建 ViewModel - 例如,在您的自定义模型绑定器中。或者您可以将 Jimmy Bogard 的方法与 AutoMapper 一起使用,它也支持 IoC 容器。

这里的常用方法是,每当我看到重复代码时,我都会设法消除它。执行域视图模型编组和存储库查找的 100 个控制器操作是一个坏情况。以通用方式进行的单一模型绑定器是一种很好的方式。

【讨论】:

    【解决方案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's no matching MyType in the repository
        // e.g. return RedirectToAction("Index")
      }
    }
    

    模型绑定器:-

    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。

    【讨论】:

    • 您的想法对我很有吸引力,我希望订阅您的时事通讯。
    【解决方案4】:

    这是另一个解决方案: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”就不会凌乱。

    就像在这个模型中:

    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(也称为依赖注入)。这会从您的控制器中删除模型填充代码,并允许它专注于控制应用程序的逻辑流程。自定义 ViewModel 是抽象您的下拉列表所依赖的 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() 方法,这将采用 Spouse 对象。这个概念被称为 AggregateRoots,从根控制所有依赖项——即 Employee() 对象。但是,那只是我。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-04-11
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多