ViewModel 最佳实践
-
21-08-2019 - |
题
从 这个问题, ,看起来让控制器创建一个是有意义的 视图模型 这更准确地反映了视图试图显示的模型,但我对一些约定很好奇(我是 MVC 模式的新手,如果它还不是很明显的话)。
基本上,我有以下问题:
- 我通常喜欢有一个类/文件。这有意义吗? 视图模型 如果创建它只是为了将数据从控制器传递到视图?
- 如果一个 视图模型 确实属于它自己的文件,并且您正在使用目录/项目结构来将事物分开,那么 视图模型 文件所属?在里面 控制器 目录?
现在基本上就是这样。我可能还会有一些问题,但这在过去一个小时左右一直困扰着我,我似乎可以在其他地方找到一致的指导。
编辑:查看样本 书呆子晚餐应用程序 在 CodePlex 上,ViewModel 看起来像是 控制器, ,但它们不在自己的文件中仍然让我感到不舒服。
解决方案
我为每个视图创建一个所谓的“ViewModel”。我将它们放在 MVC Web 项目中名为 ViewModels 的文件夹中。我根据它们代表的控制器和操作(或视图)来命名它们。因此,如果我需要将数据传递到 Membership 控制器上的 SignUp 视图,我将创建一个 MembershipSignUpViewModel.cs 类并将其放入 ViewModels 文件夹中。
然后,我添加必要的属性和方法,以方便将数据从控制器传输到视图。我使用 Automapper 从 ViewModel 到域模型,并在必要时再次返回。
这也适用于包含其他 ViewModel 类型的属性的复合 ViewModel。例如,如果您在成员资格控制器的索引页面上有 5 个小部件,并且为每个部分视图创建了一个 ViewModel - 如何将数据从 Index 操作传递到部分视图?您向 MyPartialViewModel 类型的 MembershipIndexViewModel 添加一个属性,并且在渲染部分时您将传入 Model.MyPartialViewModel。
通过这种方式,您可以调整部分 ViewModel 属性,而无需更改 Index 视图。它仍然只是传入 Model.MyPartialViewModel,因此当您所做的只是向部分 ViewModel 添加属性时,您不必遍历整个部分链来修复某些内容的可能性较小。
我还将命名空间“MyProject.Web.ViewModels”添加到 web.config,以便允许我在任何视图中引用它们,而无需在每个视图上添加显式导入语句。只是让它变得更干净一点。
其他提示
按类别(控制器、视图模型、过滤器等)分离类是无意义的。
如果您想为网站的主页部分 (/) 编写代码,请创建一个名为 Home 的文件夹,并将 HomeController、IndexViewModel、AboutViewModel 等放入其中。以及 Home 操作使用的所有相关类。
如果您有共享类,例如 ApplicationController,您可以将其放在项目的根目录中。
为什么要把相关的东西(HomeController、IndexViewModel)分开,而把完全没有关系的东西(HomeController、AccountController)放在一起?
我写了一个 博客文章 关于这个话题。
我将应用程序类保存在名为“Core”(或单独的类库)的子文件夹中,并使用与 基格 示例应用程序,但进行了一些细微的更改以使我的应用程序更加干燥。
我在 /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();
}
我认为这非常有效,它可以让你的视图保持整洁,让你的控制器保持精简。
ViewModel 类将由类实例表示的多条数据封装到一个易于管理的对象中,您可以将其传递给视图。
将 ViewModel 类放在自己的文件、自己的目录中是有意义的。在我的项目中,我有一个名为 ViewModels 的 Models 文件夹的子文件夹。这就是我的 ViewModel(例如 ProductViewModel.cs
) 居住。
没有好的地方可以存放模型。如果项目很大并且有很多 ViewModel(数据传输对象),您可以将它们保留在单独的程序集中。您也可以将它们保存在站点项目的单独文件夹中。例如,在 氧化铁矿 它们被放置在 Oxite 项目中,该项目也包含许多不同的类。Oxite 中的控制器被移至单独的项目,视图也位于单独的项目中。
在 CodeCamp服务器 ViewModel 被命名为 *Form,它们被放置在 Models 文件夹中的 UI 项目中。
在 MvcPress 它们被放置在 Data 项目中,其中还包含使用数据库的所有代码以及更多内容(但我不推荐这种方法,它只是一个示例)
所以你可以看到有很多观点。我通常将 ViewModel(DTO 对象)保留在站点项目中。但当我有超过 10 个模型时,我更喜欢将它们分开组装。通常在这种情况下,我也会将控制器移动到单独的组件中。
另一个问题是如何轻松地将所有数据从模型映射到 ViewModel。我建议看一下 自动映射器 图书馆。我非常喜欢它,它为我做了所有脏活累活。
我也建议看看 夏普架构 项目。它为项目提供了非常好的架构,并且包含许多很酷的框架和指南以及很棒的社区。
这是我的最佳实践的代码片段:
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");
}
}
我们将所有 ViewModel 放入 Models 文件夹中(所有业务逻辑都位于单独的 ServiceLayer 项目中)
就我个人而言,我建议如果 ViewModel 不那么简单,那么就使用一个单独的类。
如果您有多个视图模型,那么我建议至少将其分区到一个目录中。如果稍后共享视图模型,则目录中隐含的名称空间可以更轻松地移动到新程序集。
在我们的例子中,我们在与视图分开的项目中拥有模型和控制器。
根据经验,我们尝试将大部分 ViewData["..."] 内容移动并避免到 ViewModel,从而避免强制转换和魔术字符串,这是一件好事。
ViewModel 还包含一些常见属性,例如列表的分页信息或用于绘制面包屑和标题的页面标题信息。目前,我认为基类包含了太多信息,我们可以将其分为三部分,基本视图模型上 99% 的页面的最基本和必要的信息,然后是列表模型和模型用于保存该场景的特定数据并继承自基础数据的表单。
最后,我们为每个实体实现一个视图模型来处理特定的信息。
控制器中的代码:
[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(单独的类库项目)
在这个项目中,我制作了一些文件夹,例如:DAL,BLL,BO,VM(视图模型的文件夹)
创建一个视图模型基类,它具有常用的必需属性,例如操作结果和上下文数据,您还可以放置当前用户数据和角色
class ViewModelBase
{
public bool HasError {get;set;}
public string ErrorMessage {get;set;}
public List<string> UserRoles{get;set;}
}
在基本控制器类中有一个像 PopulateViewModelBase() 这样的方法,该方法将填充上下文数据和用户角色。HasError 和 ErrorMessage ,如果从 service/db 提取数据时出现异常,请设置这些属性。将这些属性绑定到视图上以显示错误。用户角色可用于根据角色在视图上显示隐藏部分。
要在不同的获取操作中填充视图模型,可以通过使用具有抽象方法 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;
}
}