ASP.NET Razor:解决视图模型的一些体系结构问题

图片


引言


大家好!
今天,我想与大家分享我在开发视图模型体系结构方面的经验,这是使用Razor模板引擎开发ASP.NET Web应用程序的一部分。
本文中描述的技术实现适用于所有当前版本的ASP。 NETMVC 5Core等)。 本文本身适用于至少已经有使用此堆栈的经验的读者。 还值得注意的是,在此框架中, 我们没有考虑视图模型及其假设应用的有用性 (假定读者已经熟悉这些内容),我们直接讨论实现。


挑战赛


为了方便合理地吸收材料,我建议立即考虑将自然导致我们遇到潜在问题及其最佳解决方案的任务。
例如,这就是将新车平庸地添加到某些车辆类别中的问题 。 为了不使抽象任务复杂化,将有​​意遗漏其余方面的细节。 但是,似乎基本任务是尝试着重于系统的进一步扩展(特别是相对于属性和其他定义组件的数量扩展模型)来做所有事情,以便以后尽可能地舒适地工作。


实作


让模型看起来如下(为简单起见,未提供导航属性等):


class Transport { public int Id { get; set; } public int TransportTypeId { get; set; } public string Number { get; set; } } 

当然, TransportTypeId是类型为TransportType的对象的外键:


 class TransportType { public int Id { get; set; } public string Name { get; set; } } 

对于前端和后端之间的连接,我们将使用数据传输对象模板。 因此,用于添加汽车的DTO如下所示:


 class TransportAddDTO { [Required] public int TransportTypeId { get; set; } [Required] [MaxLength(10)] public string Number { get; set; } } 

* 使用System.ComponentModel.DataAnnotations标准验证属性。


现在是时候为汽车添加页面弄清楚View Model了。 一些开发人员会很高兴地宣布TransportAddDTO本身就是这样,但是,这根本是错误的,因为除了直接添加新元素所需的后端信息(根据定义)之外,没有任何东西可以“塞入”此类中。 另外,在添加页面上可能还需要其他数据:例如, 车辆类型的目录(基于该目录随后表示TransportTypeId )。 在这方面,以下视图模型建议自己:


 class TransportAddViewModel { public IEnumerable<TransportTypeDTO> TransportTypes { get; set; } } 

其中,在这种情况下, TransportTypeDTO将是TransportType的直接映射(这与通常情况截然不同-在截断方向和扩展方向上都是如此):


 class TransportTypeDTO { public int Id { get; set; } public string Name { get; set; } } 

在这个阶段,出现了一个合理的问题:在Razor中 ,将只能传输一个模型(感谢上帝),然后如何使用TransportAddDTO在此页面内生成HTML代码?
很简单! 只需将这个DTO添加到View Model即可 ,如下所示:


 class TransportAddViewModel { public TransportAddDTO AddDTO { get; set; } public IEnumerable<TransportTypeDTO> TransportTypes { get; set; } } 

现在第一个问题开始了。 让我们尝试在.cshtml文件中的页面上添加用于“车辆编号”的标准TextBox (将其设置为TransportAddView.cshtml):


 @model TransportAddViewModel @Html.TextBoxFor(m => m.AddDTO.Number) 

这将呈现为如下的HTML代码:


 <input id="AddDTO_Number" name="AddDTO.Number" /> 

想象一下,带有添加车辆方法的控制器部分看起来像这样( 符合MVC 5代码,对于Core来说会稍有不同,但是本质是相同的 ):


 [Route("add"), HttpPost] public ActionResult Add(TransportAddDTO transportAddDto) { //     transportAddDto... } 

在这里,我们至少看到两个问题:


  1. IdName属性具有AddDTO前缀,随后,如果使用模型绑定原理在控制器中添加传输的方法尝试将来自客户端的数据绑定到TransportAddDTO ,则内部的对象将完全由零(默认值)组成,即 它只是一个新的空实例。 这是合乎逻辑的-活页夹的预期名称为Number形式,而不是AddDTO_Number形式。
  2. 所有元属性都消失了,即 data-val-required,以及我们在AddDTO中如此仔细地描述为验证属性的所有其他参数 。 对于那些使用Razor的全部功能的用户而言,这一点至关重要,因为这对于前端而言是大量的信息丢失。
    我们很幸运,他们有相应的决定。

这些东西在使用Kendo UI的包装器时(例如@Html.Kendo().TextBoxFor()等)“起作用”。


让我们从第二个问题开始:这是因为在视图模型中,传输的TransportAddDTO实例为null 。 渲染机制的实现使得在这种情况下至少不能完全读取属性。 分别的解决方案是显而易见的-首先在视图模型中使用默认构造函数使用类的实例初始化TransportAddDTO属性。 最好在返回初始化的视图模型的服务中执行此操作,但是,作为示例的一部分,它将执行相同的操作:


 class TransportAddViewModel { public TransportAddDTO AddDTO { get; set; } = new TransportAddDTO(); public IEnumerable<TransportTypeDTO> TransportTypes { get; set; } } 

经过这些更改后,结果将类似于:


 <input data-val="true" id="AddDTO_Number" name="AddDTO.Number" data-val-required="The Number field is required." data-val-length="The field Number must be a string with a maximum length of 10." data-val-length-max="10" /> 

已经更好了! 仍然需要解决第一个问题-顺便说一下,一切都有些复杂。
为了理解它,首先您需要弄清楚什么Razor (意味着WebViewPage,可以在其中使用.cshtml的实例),这是我们为了调用TextBoxFor引用的Html属性。
查看它,您可以立即了解到它的类型为HtmlHelper<T> ,在我们的例子中为HtmlHelper<TransportAddViewModel> 。 解决该问题的可能方法是-在内部创建自己的HtmlHelper ,并将我们的TransportAddDTO作为输入传递给它。 我们为此类的实例找到最小的构造函数:


 HtmlHelper<T>.HtmlHelper(ViewContext viewContext, IViewDataContainer viewDataContainer); 

我们可以通过this.ViewContextWebViewPage实例直接传递ViewContext 。 现在,让我们找出在哪里可以实现实现IViewDataContainer接口的类的实例。 例如,创建您的实现:


 public class ViewDataContainer<T> : IViewDataContainer where T : class { public ViewDataDictionary ViewData { get; set; } public ViewDataContainer(object model) { ViewData = new ViewDataDictionary(model); } } 

如您所见,现在我们依赖于传递给构造函数的某个对象,这些对象用于初始化ViewDataDictionary ,因为这里的一切都很简单-这是View模型中我们的TransportAddDTO的一个实例。 也就是说,您可以这样获得珍贵的实例:


 var vdc = new ViewDataContainer<TransportAddDTO>(Model.AddDTO); 

因此,创建新的HtmlHelper也不存在任何问题:


 var Helper = new HtmlHelper<T>(this.ViewContext, vdc); 

现在您可以使用以下内容:


 @model TransportAddViewModel @{ var vdc = new ViewDataContainer<TransportAddDTO>(Model.AddDTO); var Helper = new HtmlHelper<T>(this.ViewContext, vdc); } @Helper.TextBoxFor(m => m.Number) 

这将呈现为如下的HTML代码:


 <input data-val="true" id="Number" name="Number" data-val-required="The Number field is required." data-val-length="The field Number must be a string with a maximum length of 10." data-val-length-max="10" /> 

如您所见,现在呈现的元素没有问题,并且可以充分使用了。 它仅用于“梳理”代码,以使其看起来不那么庞大。 例如,我们将ViewDataContainer扩展如下:


 public class ViewDataContainer<T> : IViewDataContainer where T : class { public ViewDataDictionary ViewData { get; set; } public ViewDataContainer(object model) { ViewData = new ViewDataDictionary(model); } public HtmlHelper<T> GetHtmlHelper(ViewContext context) { return new HtmlHelper<T>(context, this); } } 

然后从Razor您可以像这样工作:


 @model TransportAddViewModel @{ var Helper = new ViewDataContainer<TransportAddDTO>(Model.AddDTO).GetHtmlHelper(ViewContext); } @Helper.TextBoxFor(m => m.Number) 

另外,没有人愿意扩展WebViewPage的标准实现,以使其包含所需的属性(带有DTO类实例的设置器)。


结论


这解决了问题,并且获得了用于Razor的View模型架构,该架构可能包含所有必要的元素。


值得注意的是,最终的ViewDataContainer证明是通用的并且适合使用。


仍然需要在我们的.cshtml文件中添加几个按钮,任务将完成(不考虑在backend'e上进行处理)。 我建议我自己做。


如果受人尊敬的读者对如何以最佳方式实现所需的想法有想法,我将很乐意听取评论。


问候
彼得·奥塞特罗夫

Source: https://habr.com/ru/post/zh-CN416315/


All Articles