如何在模型和ViewModel中“干掉”C#属性?

这个问题的灵感来自我与ASP.NET MVC的斗争,但我认为它也适用于其他情况。

假设我有一个ORM生成的模型和两个ViewModel(一个用于“详细信息”视图,一个用于“编辑”视图):

模型

public class FooModel // ORM generated { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string EmailAddress { get; set; } public int Age { get; set; } public int CategoryId { get; set; } } 

显示ViewModel

 public class FooDisplayViewModel // use for "details" view { [DisplayName("ID Number")] public int Id { get; set; } [DisplayName("First Name")] public string FirstName { get; set; } [DisplayName("Last Name")] public string LastName { get; set; } [DisplayName("Email Address")] [DataType("EmailAddress")] public string EmailAddress { get; set; } public int Age { get; set; } [DisplayName("Category")] public string CategoryName { get; set; } } 

编辑ViewModel

 public class FooEditViewModel // use for "edit" view { [DisplayName("First Name")] // not DRY public string FirstName { get; set; } [DisplayName("Last Name")] // not DRY public string LastName { get; set; } [DisplayName("Email Address")] // not DRY [DataType("EmailAddress")] // not DRY public string EmailAddress { get; set; } public int Age { get; set; } [DisplayName("Category")] // not DRY public SelectList Categories { get; set; } } 

请注意,ViewModel上的属性不是DRY – 重复了很多信息。 现在假设这个场景乘以10或100,你可以看到它很快变得非常乏味且容易出错,以确保ViewModel之间的一致性(因此也可以跨视图)。

我怎么能“干掉”这段代码?

在您回答之前,“只需将所有属性放在FooModel ,”我试过了,但它没有用,因为我需要保持我的ViewModel“平坦”。 换句话说,我不能只用模型组合每个ViewModel – 我需要我的ViewModel只有View应该使用的属性(和属性),而View不能挖掘子属性得到价值观。

更新

LukLed的回答建议使用inheritance。 这肯定减少了非DRY代码的数量,但它并没有消除它。 请注意,在上面的示例中, Category属性的DisplayName属性需要写入两次,因为属性的数据类型在显示和编辑ViewModel之间是不同的。 这在小规模上不会是一个大问题,但随着项目的规模和复杂性的扩大(想象更多的属性,每个属性更多的属性,每个模型更多的视图),仍然有可能“重复自己”相当数量。 也许我在这里干得太远了,但我仍然宁愿让我所有的“友好名字”,数据类型,validation规则等只输出一次。

我假设您这样做是为了利用HtmlHelpers EditorFor和DisplayFor,并且不希望在整个应用程序中隆重地宣布相同的事情4000次。

干掉它的最简单方法是实现自己的ModelMetadataProvider。 ModelMetadataProvider正在读取这些属性并将它们呈现给模板助手。 MVC2已经提供了一个DataAnnotationsModelMetadataProvider实现来实现这一目标,因此从中inheritance可以使事情变得非常简单。

为了帮助您入门,这是一个将camelcased属性名称拆分为空格的简单示例,FirstName => First Name:

 public class ConventionModelMetadataProvider : DataAnnotationsModelMetadataProvider { protected override ModelMetadata CreateMetadata(IEnumerable attributes, Type containerType, Func modelAccessor, Type modelType, string propertyName) { var metadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName); HumanizePropertyNamesAsDisplayName(metadata); if (metadata.DisplayName.ToUpper() == "ID") metadata.DisplayName = "Id Number"; return metadata; } private void HumanizePropertyNamesAsDisplayName(ModelMetadata metadata) { metadata.DisplayName = HumanizeCamel((metadata.DisplayName ?? metadata.PropertyName)); } public static string HumanizeCamel(string camelCasedString) { if (camelCasedString == null) return ""; StringBuilder sb = new StringBuilder(); char last = char.MinValue; foreach (char c in camelCasedString) { if (char.IsLower(last) && char.IsUpper(c)) { sb.Append(' '); } sb.Append(c); last = c; } return sb.ToString(); } } 

然后你需要做的就是注册它就像在Global.asax的Application Start中添加你自己的自定义ViewEngine或ControllerFactory:

 ModelMetadataProviders.Current = new ConventionModelMetadataProvider(); 

现在只是为了告诉你我不是在欺骗这是我用来获得相同HtmlHelper的视图模型。*。作为装饰ViewModel的体验:

  public class FooDisplayViewModel // use for "details" view { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } [DataType("EmailAddress")] public string EmailAddress { get; set; } public int Age { get; set; } [DisplayName("Category")] public string CategoryName { get; set; } } 

声明BaseModel,inheritance并添加其他属性:

 public class BaseFooViewModel { [DisplayName("First Name")] public string FirstName { get; set; } [DisplayName("Last Name")] public string LastName { get; set; } [DisplayName("Email Address")] [DataType("EmailAddress")] public string EmailAddress { get; set; } } public class FooDisplayViewModel : BaseFooViewModel { [DisplayName("ID Number")] public int Id { get; set; } } public class FooEditViewModel : BaseFooViewModel 

编辑

关于类别。 不应该编辑视图模型有public string CategoryName { get; set; } public string CategoryName { get; set; }public List Categories { get; set; } public List Categories { get; set; } 而不是SelectList? 这样你可以放置public string CategoryName { get; set; } public string CategoryName { get; set; } 在基类中并保持干燥。 编辑视图通过添加List增强类。

正如LukLed所说,您可以创建View和Edit模型派生的基类,或者您也可以从另一个派生一个视图模型。 在许多应用程序中,Edit模型与View加上一些额外的东西(如选择列表)基本相同,因此从View模型派生Edit模型可能是有意义的。

或者,如果您担心“类爆炸”,您可以为两者使用相同的视图模型,并通过ViewData传递其他内容(如SelectLists)。 我不推荐这种方法,因为我认为通过ViewData传递一些状态和其他状态是令人困惑的,但它是一个选项。

另一个选择是拥抱单独的模型。 我只是保持逻辑DRY,但我不太担心我的DTO中的一些冗余属性(特别是在使用代码生成为我生成90%视图模型的项目中)。

我注意到的第一件事 – 你有2个视图模型。 有关详细信息,请参阅我的答案。

其他已经提到的事情已经提到过(应用DRY的经典方法 – inheritance和约定)。


我想我太模糊了。 我的想法是为每个域模型创建视图模型,然后 – 将它们组合在每个特定视图的视图模型中。 在你的情况下:=>

 public class FooViewModel { strange attributes everywhere tralalala firstname,lastname,bar,fizz,buzz } public class FooDetailsViewModel { public FooViewModel Foo {get;set;} some additional bull**** if needed } public class FooEditViewModel { public FooViewModel Foo {get;set;} some additional bull**** if needed } 

这允许我们创建更复杂的视图模型(每个视图)=>

 public class ComplexViewModel { public PaginationInfo Pagination {get;set;} public FooViewModel Foo {get;set;} public BarViewModel Bar {get;set;} public HttpContext lol {get;set;} } 

你可能会觉得我的这个问题很有用。

嗯……结果我确实建议创建3个视图模型。 无论如何,那段代码片段反映了我的方法。

另一个提示 – 我将使用基于filter和约定(例如,基于类型)的机制,用必要的selectList填充viewdata(mvc框架可以通过名称或其他东西自动绑定viewData中的selectList)。

另一个提示 – 如果你使用AutoMapper来管理你的视图模型,它有很好的function – 它可以展平对象图 。 因此 – 您可以创建直接具有视图模型道具(每个域模型)的视图模型(每个视图),无论您想要多深入(Haack说它没关系)。

这些显示名称(值)可能会显示在具有大量const字段的另一个静态类中。 不会保存您有很多DisplayNameAttribute实例,但它会使名称更改快速,轻松。 显然这对其他元属性没有帮助。

如果我告诉我的团队他们将不得不为相同数据的每个小排列创建一个新模型(并随后为它们编写自动化定义),他们就会反抗和诽谤我。 我宁愿模拟元数据,这些元数据在某种程度上也是可以使用的。 例如,使属性必需属性仅在“添加”(Model == null)方案中生效。 特别是因为我甚至不会写两个视图来处理添加/编辑。 我会有一个视图来处理它们两个,如果我开始有不同的模型类,我会遇到我的父类声明… … ViewPage位。