ViewModel 最佳实践

Posted

技术标签:

【中文标题】ViewModel 最佳实践【英文标题】:ViewModel Best Practices 【发布时间】:2010-10-14 10:24:13 【问题描述】:

从this question 看来,让控制器创建一个 ViewModel 来更准确地反映视图试图显示的模型似乎是有意义的,但我对其中的一些感到好奇约定(我是 MVC 模式的新手,如果不是很明显的话)。

基本上,我有以下问题:

    我通常喜欢有一个类/文件。如果 ViewModel 只是为了将数据从控制器传递到视图而创建的,这对它有意义吗? 如果 ViewModel 确实属于其自己的文件,并且您使用目录/项目结构将其分开,那么 ViewModel 文件属于哪里?在 Controllers 目录中?

现在基本上就是这样。我可能还有一些问题要问,但这在过去一个小时左右一直困扰着我,我似乎可以在其他地方找到一致的指导。

编辑: 查看 CodePlex 上的示例 NerdDinner app,看起来 ViewModel 是 Controllers 的一部分,但它们不在自己的文件中仍然让我感到不舒服。

【问题讨论】:

我不会将 NerdDinner 称为“最佳实践”示例。你的直觉很适合你。 :) 【参考方案1】:

我为每个视图创建一个我称之为“ViewModel”的东西。我将它们放在我的 MVC Web 项目中名为 ViewModels 的文件夹中。我以它们所代表的控制器和操作(或视图)来命名它们。因此,如果我需要将数据传递给 Membership 控制器上的 SignUp 视图,我会创建一个 MembershipSignUpViewModel.cs 类并将其放入 ViewModels 文件夹中。

然后我添加必要的属性和方法以方便将数据从控制器传输到视图。我使用 Automapper 从我的 ViewModel 到 Domain Model 并在必要时再次返回。

这也适用于包含其他 ViewModel 类型的属性的复合 ViewModel。例如,如果您在成员控制器的索引页面上有 5 个小部件,并且为每个局部视图创建了一个 ViewModel - 您如何将数据从索引操作传递给局部视图?您向 MyPartialViewModel 类型的 MembershipIndexViewModel 添加一个属性,并在渲染部分时传入 Model.MyPartialViewModel。

这样做可以让您调整部分 ViewModel 属性,而无需更改索引视图。它仍然只是在 Model.MyPartialViewModel 中传递,因此当您所做的只是向部分 ViewModel 添加属性时,您必须通过整个部分链来修复某些东西的可能性较小。

我还将命名空间“MyProject.Web.ViewModels”添加到 web.config,以便允许我在任何视图中引用它们,而无需在每个视图上添加显式导入语句。只是让它更干净一点。

【讨论】:

如果你想从局部视图 POST 并返回整个视图(以防模型错误)怎么办?在局部视图中,您无权访问父模型。 @Cosmo:然后 POST 到一个 action,该动作可以在模型错误的情况下返回整个视图。在服务器端,您有足够的资源重新创建父模型。 登录 [POST] 和登录 [GET] 操作怎么样?使用不同的视图模型? 通常登录[GET]不会调用ViewModel,因为不需要加载任何数据。 很好的建议。模型/VM属性的数据访问、处理和设置应该去哪里?在我的例子中,我们将有一些来自本地 CMS 数据库的数据和一些来自 Web 服务的数据,这些数据需要在模型上设置之前进行处理/操作。将所有这些都放在控制器中会变得非常混乱。【参考方案2】:

按类别(控制器、视图模型、过滤器等)分离类是无稽之谈。

如果您想为网站的 Home 部分 (/) 编写代码,请创建一个名为 Home 的文件夹,并将 HomeController、IndexViewModel、AboutViewModel 等以及 Home 操作使用的所有相关类放在那里。

如果您有共享类,例如 ApplicationController,您可以将其放在项目的根目录中。

为什么要将相关的东西(HomeController、IndexViewModel)分开,而把完全没有关系的东西(HomeController、AccountController)放在一起?


我就这个话题写了blog post。

【讨论】:

如果你这样做,事情很快就会变得非常混乱。 不,混乱是将所有控制器放在一个目录/命名空间中。如果你有 5 个控制器,每个控制器使用 5 个视图模型,那么你就有 25 个视图模型。命名空间是组织代码的机制,在这里应该没有什么不同。 @Max Toro:很惊讶你被如此反对。在 ASP.Net MVC 上工作了一段时间后,我感到 很多 痛苦,因为将 所有 ViewModel 放在一个地方,所有 控制器在另一个中,所有视图在另一个中。 MVC 是三个相关的部分,它们耦合的——它们相互支持。如果给定部分的控制器、视图模型和视图一起存在于同一个目录中,我觉得一个解决方案可以让我更有条理。 MyApp/Accounts/Controller.cs、MyApp/Accounts/Create/ViewModel.cs、MyApp/Accounts/Create/View.cshtml @RyanJMcGowan 关注点分离不是类分离。 @RyanJMcGowan 我想补充 Max 的评论并声明即使在最初的开发过程中,我们也希望逐个功能地工作。逐个特征是例如采用的方法。 SCRUM 开发过程,其中每个故事都增加了业务价值。仅花了 2 个月开发视图模型后,有效增加的业务价值仍然为零,因为实际上没有任何东西可用。【参考方案3】:

我将我的应用程序类保存在一个名为“Core”(或单独的类库)的子文件夹中,并使用与 KIGG 示例应用程序相同的方法,但稍作更改以使我的应用程序更加干燥。

我在 /Core/ViewData/ 中创建了一个 BaseViewData 类,我在其中存储了常用的站点范围属性。

在此之后,我还在同一个文件夹中创建了我的所有视图 ViewData 类,然后从 BaseViewData 派生并具有视图特定属性。

然后我创建一个 ApplicationController,我的所有控制器都从该控制器派生。 ApplicationController 有一个通用的 GetViewData 方法如下:

protected T GetViewData<T>() where T : BaseViewData, new()
    
        var viewData = new T
        
           Property1 = "value1",
           Property2 = this.Method() // in the ApplicationController
        ;
        return viewData;
    

最后,在我的控制器操作中,我执行以下操作来构建我的 ViewData 模型

public ActionResult Index(int? id)
    
        var viewData = this.GetViewData<PageViewData>();
        viewData.Page = this.DataContext.getPage(id); // ApplicationController
        ViewData.Model = viewData;
        return View();
    

我认为这非常有效,它可以让你的视图保持整洁,让你的控制器保持精简。

【讨论】:

【参考方案4】:

ViewModel 类用于将由类实例表示的多条数据封装到一个易于管理的对象中,您可以将其传递给您的视图。

将 ViewModel 类放在自己的文件中,在自己的目录中是有意义的。在我的项目中,我有一个名为 ViewModels 的 Models 文件夹的子文件夹。这就是我的 ViewModel(例如 ProductViewModel.cs)所在的位置。

【讨论】:

【参考方案5】:

没有什么好地方可以保存模型。如果项目很大并且有很多 ViewModel(数据传输对象),您可以将它们保存在单独的组件中。您也可以将它们保存在站点项目的单独文件夹中。例如,在Oxite 中,它们被放置在 Oxite 项目中,该项目也包含许多不同的类。 Oxite 中的控制器被移动到单独的项目中,视图也在单独的项目中。 在CodeCampServer 中,ViewModel 被命名为 *Form,它们被放置在 UI 项目的 Models 文件夹中。 在MvcPress 项目中,它们被放置在数据项目中,该项目还包含与数据库一起使用的所有代码等等(但我不推荐这种方法,它只是一个示例) 所以你可以看到有很多观点。我通常将我的 ViewModel(DTO 对象)保存在站点项目中。但是当我有超过 10 个模型时,我更喜欢将它们移动到单独的装配中。通常在这种情况下,我也会将控制器移动到单独的组件中。 另一个问题是如何轻松地将所有数据从模型映射到您的 ViewModel。我建议看看AutoMapper library。我非常喜欢它,它为我做了所有肮脏的工作。 我也建议看看SharpArchitecture 项目。它为项目提供了非常好的架构,并且包含许多很酷的框架和指南以及很棒的社区。​​p>

【讨论】:

【参考方案6】:

这是我的最佳实践中的代码 sn-p:

    public class UserController : Controller
    
        private readonly IUserService userService;
        private readonly IBuilder<User, UserCreateInput> createBuilder;
        private readonly IBuilder<User, UserEditInput> editBuilder;

        public UserController(IUserService userService, IBuilder<User, UserCreateInput> createBuilder, IBuilder<User, UserEditInput> editBuilder)
        
            this.userService = userService;
            this.editBuilder = editBuilder;
            this.createBuilder = createBuilder;
        

        public ActionResult Index(int? page)
        
            return View(userService.GetPage(page ?? 1, 5));
        

        public ActionResult Create()
        
            return View(createBuilder.BuildInput(new User()));
        

        [HttpPost]
        public ActionResult Create(UserCreateInput input)
        
            if (input.Roles == null) ModelState.AddModelError("roles", "selectati macar un rol");

            if (!ModelState.IsValid)
                return View(createBuilder.RebuildInput(input));

            userService.Create(createBuilder.BuilEntity(input));
            return RedirectToAction("Index");
        

        public ActionResult Edit(long id)
        
            return View(editBuilder.BuildInput(userService.GetFull(id)));
        

        [HttpPost]
        public ActionResult Edit(UserEditInput input)
                   
            if (!ModelState.IsValid)
                return View(editBuilder.RebuildInput(input));

            userService.Save(editBuilder.BuilEntity(input));
            return RedirectToAction("Index");
        

【讨论】:

【参考方案7】:

我们将所有的 ViewModel 都放在 Models 文件夹中(我们所有的业务逻辑都在一个单独的 ServiceLayer 项目中)

【讨论】:

【参考方案8】:

我个人建议如果 ViewModel 不是微不足道的,那么请使用单独的类。

如果您有多个视图模型,那么我建议将其分区到至少一个目录中是有意义的。如果稍后共享视图模型,则目录中隐含的名称空间可以更轻松地移动到新程序集。

【讨论】:

【参考方案9】:

在我们的例子中,我们将模型和控制器放在一个独立于视图的项目中。

根据经验,我们已尝试将大部分 ViewData["..."] 内容移动并避免到 ViewModel,因此我们避免了强制转换和魔术字符串,这是一件好事。

ViewModel 还包含一些常见属性,例如列表的分页信息或用于绘制面包屑和标题的页面标题信息。目前基类在我看来包含的信息太多,我们可以将它分为三部分,基本视图模型上 99% 页面的最基本和必要的信息,然后是列表模型和模型对于包含该场景的特定数据并从基础数据继承的表单。

最后,我们为每个实体实现一个视图模型来处理具体的信息。

【讨论】:

【参考方案10】:

控制器中的代码:

    [HttpGet]
        public ActionResult EntryEdit(int? entryId)
        
            ViewData["BodyClass"] = "page-entryEdit";
            EntryEditViewModel viewMode = new EntryEditViewModel(entryId);
            return View(viewMode);
        

    [HttpPost]
    public ActionResult EntryEdit(Entry entry)
    
        ViewData["BodyClass"] = "page-entryEdit";            

        #region save

        if (ModelState.IsValid)
        
            if (EntryManager.Update(entry) == 1)
            
                return RedirectToAction("EntryEditSuccess", "Dictionary");
            
            else
            
                return RedirectToAction("EntryEditFailed", "Dictionary");
            
        
        else
        
            EntryEditViewModel viewModel = new EntryEditViewModel(entry);
            return View(viewModel);
        

        #endregion
    

视图模型中的代码:

public class EntryEditViewModel
    
        #region Private Variables for Properties

        private Entry _entry = new Entry();
        private StatusList _statusList = new StatusList();        

        #endregion

        #region Public Properties

        public Entry Entry
        
            get  return _entry; 
            set  _entry = value; 
        

        public StatusList StatusList
        
            get  return _statusList; 
        

        #endregion

        #region constructor(s)

        /// <summary>
        /// for Get action
        /// </summary>
        /// <param name="entryId"></param>
        public EntryEditViewModel(int? entryId)
        
            this.Entry = EntryManager.GetDetail(entryId.Value);                 
        

        /// <summary>
        /// for Post action
        /// </summary>
        /// <param name="entry"></param>
        public EntryEditViewModel(Entry entry)
        
            this.Entry = entry;
        

        #endregion       
    

项目:

DevJet.Web(ASP.NET MVC Web 项目)

DevJet.Web.App.Dictionary(一个 单独的类库项目)

在这个项目中,我制作了一些文件夹,例如: 达尔, BLL, 宝, VM(视图模型的文件夹)

【讨论】:

你好,能分享一下Entry类的结构吗?【参考方案11】:

创建一个视图模型基类,它具有操作结果和上下文数据等常用属性,您还可以放置当前用户数据和角色

class ViewModelBase 

  public bool HasError get;set; 
  public string ErrorMessage get;set;
  public List<string> UserRolesget;set;

在基本控制器类中有一个类似 PopulateViewModelBase() 的方法,该方法将填充上下文数据和用户角色。 HasError 和 ErrorMessage ,如果在从 service/db 提取数据时出现异常,请设置这些属性。在视图上绑定这些属性以显示错误。 用户角色可用于根据角色在视图上显示隐藏部分。

要在不同的 get 操作中填充视图模型,可以通过具有抽象方法 FillModel 的基本控制器来使其保持一致

class BaseController :BaseController 

   public PopulateViewModelBase(ViewModelBase model) 

   //fill up common data. 

abstract ViewModelBase FillModel();

在控制器中

class MyController :Controller 


 public ActionResult Index() 

   return View(FillModel()); 


ViewModelBase FillModel() 
 
    ViewModelBase  model=;
    string currentAction = HttpContext.Current.Request.RequestContext.RouteData.Values["action"].ToString(); 
 try 
 
   switch(currentAction) 
  
   case "Index": 
   model= GetCustomerData(); 
   break;
   // fill model logic for other actions 


catch(Exception ex) 

   model.HasError=true;
   model.ErrorMessage=ex.Message;

//fill common properties 
base.PopulateViewModelBase(model);
return model;


【讨论】:

以上是关于ViewModel 最佳实践的主要内容,如果未能解决你的问题,请参考以下文章

使用 RxSwift 将 UITableViewCell 中的控件绑定到 ViewModel 的最佳实践

Android - MVVM中ViewModel状态的最佳实践?

AndroidArchitecture Components最佳实践--Lifecycles

Android MVVM startActivity 的最佳实践

最佳实践是使用模型还是简单属性?

Combine + SwiftUI 中的最佳数据绑定实践?