【问题标题】:Entity Framework 6 Reusing Data Annotations实体框架 6 重用数据注释
【发布时间】:2014-04-30 11:21:26
【问题描述】:

我已经寻找了一段时间的最终解决方案,但尚未得出结论。我想在数据模型类上只指定一次数据注释,并让 UI 从视图模型类中看到这些注释,而无需再次指定它们。为了说明我的观点,假设我有一个这样的 UserAccount 类......

public class UserAccount
{
    [Display(Name = "Username", Prompt = "Login As"), Required()]
    string UserName { get; set; }
    [Display(Name = "Password", Prompt = "Password"), Required(), DataType(DataType.Password), StringLength(255, MinimumLength = 7, ErrorMessage = "The password must be at least 7 characters")]
    string Password { get; set; }
}

现在我想指定一个包含密码确认字段的视图模型,该字段不会存储在数据库和可能的其他数据模型中,但我不想再次指定所有数据注释属性。这是关于尝试找到最佳实践,但也需要维护多个声明,并且在某些时候一个会被修改,而其他则不会。

我查看了一个接口/继承解决方案,但由于各种原因,它并没有真正完全解决它。另一种可能的解决方案可能是在 View Model 类(或属性)上设置一个属性来表示从...继承属性,但我目前找不到任何适合此的方法。

有没有人有任何好主意或以合适的方式实施?

【问题讨论】:

    标签: c# asp.net-mvc entity-framework data-annotations


    【解决方案1】:

    你可以用这个属性装饰你的视图模型类:

    [MetadataType(typeof(YourModelClass))]
    public class YourViewModelClass
    {
       // ...
    }
    

    假设两个类具有相同的属性,注释将被正确继承。

    请参阅:MetadataTypeAttribute Class in MSDN 了解更多信息。

    注意:在 MDSN 注释中,他们解释了为现有模型类添加额外元数据的用途,但这适用于您的情况。事实上,你可以做到这一点:注释你的 ViewModel,并将它应用到你的模型的部分类。或者甚至创建一个同时应用于模型中的实体和视图模型的伙伴类。使用这些选项中的任何一个,任何类都可以“定义”自己的注释并从伙伴类“继承”其他注释。

    重要提示:该解决方案与 Hoots 接受的解决方案大不相同,因为该解决方案被 EF 或 MVC 等框架识别,用于创建模型或提供自动数据验证。 Hoots的解决方案必须手动使用,因为框架不会自动使用它。

    【讨论】:

    • 我认为这是我正在寻找的,但需要检查。这听起来类似于我正在研究的基于此博客的解决方案... jendaperl.blogspot.co.uk/2010/10/…>
    • 快速浏览了一下。它看起来很有希望,但是如果场景不同会发生什么,比如我想将一个客户数据对象和一个地址数据对象组合到一个视图模型中。根据我的阅读,您只能应用此属性一次,在这种情况下,我希望对客户和地址进行注释,但我无法指定 [MetadataType(typeof(Customer))] 和 [MetadataType(typeof(Address) )] 在同一个视图模型上。我理解正确了吗?
    • 您可以通过从视图模型中创建 3 个类来解决它:一个用于客户,一个用于地址,最后是您的 ViewModel 类,它具有客户类型的属性,以及地址类型的其他属性。在您看来,您可以参考“子属性”,MVC 可以完美地处理它们。 MVC HtmlHelpers 完美支持 Html.xxxFor(m => m.Address.City, ...)Html.TextBox("Address.City", ...)。此外,您的映射更容易进行:您可以将整个实体映射到模型中的每个属性。
    • 是的,这是传统的做事方式。我在这里有点挑剔,但是使用没有直接属性的模型视图对象没有验证。如果您检查 ModelState.IsValid,即使 m.Address.City 为“必需”且尚未设置,它也会为真,因为它是从模型视图中删除的级别。我想我已经想出了一个解决方案,我将在一秒钟内发布。我将把它留几天来设置一个作为依赖于 cmets 的答案。
    • MetadataType 对于抽象出数据注解很有用,但对于重用来说并不是真正的。据我所知,要么全有,要么全无。如果您想在两个不同的屏幕上填写 DataModel 属性,例如具有两个不同视图的向导,那该怎么办。这个问题突出了一些简短的交流stackoverflow.com/questions/19838772/… 在我的另一个问题的示例中,添加 MetadataType 不会添加任何内容,您仍然需要填写所有必需的属性,这与注释的位置无关。
    【解决方案2】:

    我查看了一个接口/继承解决方案,但由于各种原因并没有真正完全解决它。

    你能告诉我们为什么继承对你不起作用吗?

    以下是继承解决方案的示例:

    public class UserAccount : PasswordModel
    {
        [Display(Name = "Username", Prompt = "Login As"), Required()]
        public string UserName { get; set; }
    
        [Display(Name = "Password", Prompt = "Password")]
        [StringLength(255, MinimumLength = 7, ErrorMessage = "The password must be at least 7 characters")]
        [Required]
        [DataType(DataType.Password)]
        public string Password { get; set; }
    }
    
    public class ResetPasswordViewModel : PasswordModel
    {
        [Display(Name = "Retype Password", Prompt = "Password")]
        [Required]
        [DataType(DataType.Password)]
        [CompareAttribute("Password", ErrorMessage = "Password does not match")]
        string RetypePassword { get; set; }
    }
    

    更新:

    从M-V-C,你可以将设计模式扩展到M-VM-V-C

    视图模型

    VM 代表 ViewModel。这些家伙就像模型,但他们不代表领域他们没有持久化。它们代表 UI。

    关于 ViewModel 的更多信息

    根据前面的示例,ResetPasswordViewModel 是一个代表 UI 的 ViewModel。如果您担心持久化RetypePassword,请不要在 EF 中添加 ViewModel。

    另一种方法

    您可以用组合替换继承。请参阅下面的示例。

    public class ResetPasswordViewModel
    {
        public UserAccount UserAccount { get; set; }
    
        [Display(Name = "Retype Password", Prompt = "Password")]
        [Required]
        [DataType(DataType.Password)]
        public string RetypePassword { get; set; }
    }
    

    使用上面的 ViewModel,您可以获得 Password 属性的可重用性。您可以像这样访问它:

    @Model.UserAccount.Password
    @Model.ResetPassword
    

    我可能想在同一个视图中组合不同的数据模型

    这正是 ViewModel 的用途。您专门为视图创建它们。

    【讨论】:

    • 首先,我可能想在同一个视图中组合不同的数据模型。其次,尽管并非不可能,但仅保留所需数据存在一些复杂性,即我不想保留重新输入的密码确认。
    • 嗨 Yorro,是的,我说的是视图模型。感谢更新。请参阅 JotaBe 中关于视图模型的回答中的 cmets。基本上继承不适用于两个独立的对象,例如同一视图模型中的员工和部门。封装在一定程度上起作用,但 ModelState.IsValid 根据当前对象报告 true,它不检查封装的对象。我已经制定了一个我认为可以满足我要求的解决方案。 (看我目前的解决方案,欢迎cmets)
    【解决方案3】:

    您要求互斥的东西...您想继承元数据,但不想以任意方式继承元数据...这通常不是一个有用的场景。与维护单独的模型相比,您将花费更多的时间来寻找一些灵丹妙药的方案,而且您最终可能仍会得到一个不是 100% 满意的低于标准的解决方案。

    我知道您的沮丧..您不应该在多个地方维护相同的数据..您违反了 DRY 原则..不要重复自己...

    问题是,您创建的任何解决方案都将违反其他原则,例如开放封闭原则或单一责任原则。很多时候,这些原则是相互矛盾的,你必须在某个地方找到平衡。

    简单地说,最佳实践是维护单​​独的视图和域模型,具有单独的数据属性,因为您的域和您的视图是独立的关注点,它们应该分开。如果他们没有单独的关注点,您可以简单地对两者使用相同的模型,而无需跳过这些环节。

    视图模型是独立的,因为您的视图通常具有与您的域不同的要求。试图将两者混为一谈只会带来痛苦。

    【讨论】:

    • 我很欣赏 cmets 和情绪,但我并不完全同意。虽然在某些情况下可以有重复的数据或重复的代码,但您的建议类似于说有 3 个对象一个人、学生和教师,并分别对它们进行编码,而不是让学生和教师从 Person 继承。除了与其使用的数据相关的规则之外,视图模型还包含自己的一组业务规则,但这并不意味着它们不应该重用可用的代码和数据。
    • @Hoots - 我同意某些类型的元数据可以很好地继承。例如,字段长度就是一个很好的例子。在极少数用例中,您可能希望 UI 具有比数据模型更大的字段长度(但我能想到一些)。但是其他的,比如Required,则更加模糊。您的数据模型可能不需要字段,但您可能需要它的条件情况。不幸的是,当您继承元数据时,它是一个全有或全无的命题。不幸的是,属性是这些问题的课程粒度解决方案。
    • 对数据模型属性的要求就像在数据库中使用非空值一样。如果它可以为空,您仍然可以要求前端要求它。如果它不为空,则必须让前端要求它,否则插入操作将失败。查看模型所需的数据模型也是如此。你看到我沿着属性继承线提出的解决方案了吗?它不需要您在视图模型中具有相同的注释,但是如果您需要,可以选择继承和覆盖它们。
    • @Hoots - 不,您可能希望您的视图不需要该字段,因为您的数据模型需要它。例如,您可以将其留空并在留空时计算值,或者如果留空则插入默认值(想想当前日期/时间)。这就是重点,视图模型与数据模型有不同的要求,除了非常简单的情况。
    • 我认为我们同意,我们只是用不同的方式说同样的话。
    【解决方案4】:

    这是我在考虑了其他可能的解决方案后提出的解决方案。它基于找到以下文章...

    http://jendaperl.blogspot.co.uk/2010/10/attributeproviderattribute-rendered.html

    这篇文章的底部说明了我这样做的原因。

    拥有一个现有的数据模型类...

    public class UserAccount
    {
         [Display(Name = "Username", Prompt = "Login As"), Required()]
         string UserName { get; set; }
         [Display(Name = "Password", Prompt = "Password"), Required(), DataType(DataType.Password), StringLength(255, MinimumLength = 7, ErrorMessage = "The password must be at least 7 characters")]
         string Password { get; set; }
    }
    

    如果我们可以将属性属性复制到使用它的视图中,这不是很好吗,如果需要的话可以覆盖它们,这样使用这个类的视图就不需要在一个简单的属性上重新审视改变。这是我的解决方案。如下创建一个新属性...

    using System.ComponentModel;
    
    namespace MyApp.ViewModels
    {
        public class AttributesFromAttribute : AttributeProviderAttribute
        {
            public AttributesFromAttribute(Type type, string property)
                : base(type.AssemblyQualifiedName, property)
            {
            }
    
            public T GetInheritedAttributeOfType<T>() where T : System.Attribute
            {
                Dictionary<string,object> attrs = Type.GetType(this.TypeName).GetProperty(this.PropertyName).GetCustomAttributes(true).ToDictionary(a => a.GetType().Name, a => a);
                return attrs.Values.OfType<T>().FirstOrDefault();
            }
    
        }
    }
    

    现在您只需将以下内容添加到视图模型类中的相关属性...

    [AttributesFrom(typeof(MyApp.DataModel.UserAccount), "UserName")]
    

    例如...

    public class RegisterViewModel
    {
        public UserAccount UserAccount { get; set; }
        public RegisterViewModel()
        {
            UserAccount = new UserAccount();
        }
    
        [AttributesFrom(typeof(MyApp.DataModel.UserAccount), "UserName")]
        string UserName { get; set; }
        [AttributesFrom(typeof(MyApp.DataModel.UserAccount), "Password")]
        string Password { get; set; }
    
        [AttributesFrom(typeof(MyApp.DataModel.UserAccount), "Password")]
        [Display(Name = "Confirm Password", Prompt = "Confirm Password"), Compare("Password", ErrorMessage = "Your confirmation doesn't match.")]
        public string PasswordConfirmation { get; set; }
    
    }
    

    然后复制可以被覆盖的属性(如上面的 PasswordConfirmation),允许在同一个视图模型中使用多个数据模型,如果您需要从代码访问继承的属性,您可以使用 GetInheritedAttributeOfType 方法来实现。比如……

    public static class AttrHelper
    {
       public static T GetAttributeOfType<T>(this ViewDataDictionary viewData) where T : System.Attribute
        {
            var metadata = viewData.ModelMetadata;
            var prop = metadata.ContainerType.GetProperty(metadata.PropertyName);
            var attrs = prop.GetCustomAttributes(false);
    
            // Try and get the attribute directly from the property.
            T ret = attrs.OfType<T>().FirstOrDefault();
    
            // If there isn't one, look at inherited attribute info if there is any.
            if(ret == default(T))
            {
                AttributesFromAttribute inheritedAttributes = attrs.OfType<AttributesFromAttribute>().FirstOrDefault();
                if (inheritedAttributes != null)
                {
                    ret = inheritedAttributes.GetInheritedAttributeOfType<T>();
                }
            }
    
            // return what we've found.
            return ret;
        }
    }
    

    这可以从例如编辑器模板中调用...

    var dataTypeAttr = AttrHelper.GetAttributeOfType<DataTypeAttribute>(ViewData);
    

    它将首先直接查看视图模型的属性,但如果没有找到,它将查看继承的属性并调用 GetInheritedAttributeOfType。

    这对我来说效果最好,因为...

    1. 我觉得当前在视图模型和数据模型中重复 DataAnnotations 的做法不利于可维护性或重用性。

    2. 使用 MetadataType 也是不灵活的,要么全有,要么全无,你不能在单个 ViewModel 上包含多个 MetadataType 属性。

    3. 缺乏将数据模型封装在没有属性的视图模型中,因为它也没有灵活性。您必须包含整个封装对象,因此无法将 DataModel 填充到多个视图中。

    【讨论】:

    • 这个解决方案和MetaDataAttribute有一个重要的区别:元数据属性被框架识别,例如在MVC视图上创建自动验证,而另一个解决方案不是由框架识别,必须手动使用,使用GetAttributeOfType。这取决于您要选择一种或另一种解决方案,但它们的工作方式非常不同。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-08-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多