ASP MVC ViewModels和EditorTemplates中DRY数据注释的最佳实践

时间:2012-08-02 12:31:28

标签: asp.net-mvc asp.net-mvc-3 viewmodel data-annotations dry

我有以下工作系统并正在寻找使其干燥的方法:

public class EMailMetaData
{
  [Display(Prompt="myemail@mydomain.com"])
  public string Data;
}
public class PhoneMetaData
{
  [Display(Prompt="+1 (123) 456-7890"])
  public string Data;
}
public class AddressMetaData
{
  [Display(Prompt="Central st. W., St Francisco, USA"])
  public string Data;
}
// 7 more metadata templates

public class ContactVM
{
  [Required]
  public string DataLabel { get; set; }

  [Required(ErrorMessage="Please fill in the data field")]
  public string Data { get; set; }
}

[MetadataType(typeof(EmailMetaData))]
EmailVM : ContactVM
{
}
[MetadataType(typeof(PhoneMetaData))]
PhoneVM : ContactVM
{
}
[MetadataType(typeof(AddressMetaData))]
AddressVM : ContactVM
{
}
// 7 more contact view models

Controller显然是使用正确的内容初始化它们,并且在视图中我运行了每个联系人具有TemplateEditor的ContactVMs的foreach循环:EmailVM.cshtml,PhoneVM.cshtml,AddressVM.cshtml UrlVM.cshtml等。

主视图看起来(省略所有设置和详细信息如下:

@model ContactsVM
foreach (var contact in Model.Contacts)
{
  @Html.EditorFor(m => contact)
}

并在EditorTemplates下

@model EmailVM
@Html.EditorFor(model => model.DataLabel)
@Html.EditorFor(model => model.Data)
<br />
@Html.ValidationMessageFor(model => model.DataLabel)
@Html.ValidationMessageFor(model => model.Data)

...显然我已定义的每个视图模型都有更少的编辑器模板。

所以简单来说 - 非常相似的联系类型在水印,命名,验证方面存在细微差别,但基本上所有字符串都具有相同的字段(地址是一个长字符串而不是结构,所有字符都相同)。 / p>

我的问题不是特定于水印,它可以是任何属性 - 名称,描述,提示等。

[Display(Name="name", Description="description", Prompt="prompt")]

它完全正常工作并显示正确的标签,每个水印,但它似乎是巨大的DRY违规,因为除模型类型外,所有模板编辑器都完全相同。我在这里展示的是简化以集中精力处理手头的问题,主视图和编辑器模板比你在这里看到的要复杂得多,所以重复是巨大的。

你们中的任何人都可以提出更好的方法吗?不要复制那么多代码吗?

谢谢!

4 个答案:

答案 0 :(得分:2)

(我正在添加另一个答案,因为这是解决问题的完全不同的方法)

视图模型和视图的结构都表明我们正在查看一些元元数据,这是我讨厌的模式,抱歉。

让我们首先采取直截了当的方法,找出我们的模型。我想它是这样的:

public class ContactsVM
{
    [Required]
    public string EmailLabel {get;set;}

    [Display(Prompt="myemail@mydomain.com"])
    [Required(ErrorMessage="Please fill in the data field")]
    public string Email {get;set;}

    [Required]
    public string PhoneLabel {get;set;}

    [Display(Prompt="+1 (123) 456-7890"])
    [Required(ErrorMessage="Please fill in the data field")]
    public string Phone {get;set;}

    [Required]
    public string AddressLabel {get;set;}

    [Display(Prompt="Central st. W., St Francisco, USA"])
    [Required(ErrorMessage="Please fill in the data field")]
    public string Address {get;set;}

    // 7 more property pairs
}

简单,简单,直接,易于理解。是的,它需要视图中的一些代码重复(意味着两个EditorFor s后跟ValidationMessage),但根据我的经验,这不是问题,因为在大多数情况下,有一天你将不得不以某种方式调整这个一个(也是唯一一个)属性的代码。如果您不喜欢这样 - 现在,还有一个解决方案:

@* let's assume that props is a string[] holding meaningful property names, e.g "Email", "Phone", "Address". You can even get it dynamically from reflection
@foreach (var property in props)
{
    @Html.Editor(property + "Label") @Html.Editor(property)
    <br />
    @Html.Validation(property + "Label") @Html.Validation(property)
}

动态模型更新

现在,如果我们在模型中有可变数量的数据项(我现在每天都在讨论这个问题时会更加讨厌),上面的所有内容都不会起作用,所以现在我们必须应对太。我们试图实现的是与上面相同的视图代码,但现在我们的模型将不包含这些属性。所有的魔力都在于两件事。

  1. 我们将模型转换为Dictionary - 类似,包含指向字符串数据的字符串键。我们如何填充它并不重要。
  2. 我们提供自己的ModelMetadataProvider,它将覆盖我们的模型类的默认行为,具体如下:
    1. 给定模型实例,它将通过枚举上述字典并从这些字符串创建GetProperties来响应ModelMetadata
    2. 每个创建的ModelMetadata都应包含与我们的字典中的值相对应的ModelValue
    3. ModelMetadata的所有其他属性都是根据我们的意愿填充的(我们不再需要DataAnnotations,我们将在提供者中注入所有值)
  3. 这需要大量的工作和一些尝试,但最终它有效,我会从我的经验说。但我不推荐这种整体设计方法,因为它需要......你已经看到了大量的工作,而这只是一个开始。

答案 1 :(得分:1)

(您的问题源于您的视图模型不是特定的,而是一种“针对每个问题的一种通用解决方案”。)

但是,仍然有一个非常简单的解决方案:为所有类型创建一个名为的编辑器模板,将ContactVM作为模型类型,并使用@Html.EditorFor(m => contact, "YourTemplateName")

答案 2 :(得分:1)

回答我自己的问题:

它是直接的解决方案,但比建议的更简单,最重要的是与简单的解决方案相比需要最少的重复(尽管考虑到我们处于面向对象的世界并不太优雅):

  1. 使用基类(或接口,但是您定义它)​​定义一个视图作为模型。
  2. 在视图中将模型变量强制转换为正确的类
  3. 在视图中,使用cast变量而不是模型
  4. 所以Contact.cshtml看起来像这样:

    @model ContactVM
    @* Do tons of stuff that is the same between views (not depending on data annotation) *@
    @Html.Partial("_ContactDataAnnotation", Model)
    @* Continue doing lots of stuff that is the same between all those classes
    

    调用Contact.cshtml编辑器模板将采用以下方式(感谢Serg):

    @foreach (var c in Model.Contacts)
    {
      @Html.EditorFor(m => c, "Contact")
    }
    

    专用于仅显示正确数据注释的部分视图_ContactDataAnnotation.cshtml将如下所示:

    @using <My Model Namespace>
    @model ContactVM
    @switch (Model.GetType().Name)
    {
      case "EmailVM":
        EmailVM e = Model as EmailVM;
        @Html.EditorFor(model => e.DataLabel)
        @Html.EditorFor(model => e.Data)
        <br />
        @Html.ValidationMessageFor(model => e.DataLabel)
        @Html.ValidationMessageFor(model => e.Data)
        break;
      case "PhoneVM":
        PhoneVM p = Model as PhoneVM;
        @Html.EditorFor(model => p.DataLabel)
        @Html.EditorFor(model => p.Data)
        <br />
        @Html.ValidationMessageFor(model => p.DataLabel)
        @Html.ValidationMessageFor(model => p.Data)
        break;
      // same thing 7 more times for all children of ContactVM, since MVC does not applying polymorphism to data annotations UNFORTUNATELY
      default:
        @Html.EditorFor(model => model.DataLabel)
        @Html.EditorFor(model => model.Data)
        <br />
        @Html.ValidationMessageFor(model => model.DataLabel)
        @Html.ValidationMessageFor(model => model.Data)
      }
    

    通过这种方式,复制只会存在于需要的地方,而不是使用不同的名称和模型类重复相同的编辑器模板10次。

    我知道它与“最佳实践”问题的标题相矛盾,但不幸的是,这是最简单,最简约的方法。正如Serg指出的那样,其他解决方案过于复杂,需要深入的MVC基础设施干预,我不喜欢花时间,并且提示和工具提示注入而不是在数据注释中定义,这似乎是一个标准装饰和验证模型的方式。

    我认为我的解决方案是一种解决方法MVC限制缺乏数据注释的多态性。

答案 3 :(得分:0)

您应该能够创建界面,例如

public interface IContactData
{
string Data{get; set;};
.
.
.
}

类中接口的工具

public class EMailMetaData : IContactData
{
  [Display(Prompt="myemail@mydomain.com"])
  public string Data{get; set;};
...
}
public class PhoneMetaData  : IContactData
{
  [Display(Prompt="+1 (123) 456-7890"])
  public string Data{get; set;};
....
}

在您的编辑器模板中(一个人使用)

@model IContactData