65.9K
CodeProject 正在变化。 阅读更多。
Home

运行时从数据库为模型字段提供 Html.LabelFor 值

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2012年11月7日

CPOL

5分钟阅读

viewsIcon

32775

downloadIcon

301

使用 ModelMetadataProvider 为运行时模型字段设置显示名称元数据。

引言

在我开发的第一个 MVC 应用程序的过程中,我实现了一种方法来存储数据库实体框架模型中每个字段的标签文本,这样我就可以轻松地更改标签而无需任何编译,并且每个字段在所有数据录入表单和网格列标题中都有统一的标签。这种方法特别适用于数据库优先开发方法,在这种方法中,为所有字段定义 DisplayName 属性非常困难且耗时。

为了实现这一点,我派生了一个新的 DataAnnotationsModelMetadataProvider 类,顾名思义,它负责在运行时为模型提供数据注释。我将这个类命名为 DisplayNameMetadataProvider,它使用数据库表来查找每个模型字段的相应标签。

背景

据我所知,还有另外两种定义模型字段标签文本的方法;一种是传统方法,另一种是“编译时 DisplayName 属性”方法。在传统方法中,文本被分配为 HTML 页面中每个模型字段的标签。虽然这是最容易使用的方法,但在单个模型字段出现在多个页面时,它是最难维护的;您需要了解该字段出现在哪些页面上,并在所有这些页面中更改其标签。

在“编译时 DisplayName 属性”方法中,我们为模型的每个字段分配一个 DisplayName 属性,然后我们在 HTML 页面中使用 Html.LabelFor 来显示它。当我们在开发过程中使用 Code First 方法手动编写模型类时,此方法非常有用;这样我们就可以毫不犹豫地轻松地为字段添加 DisplayName 属性。尽管每次更改此属性时我们都需要重新编译项目,但这还算不上令人烦恼。 

然而,当您不通过编码创建类时,无论是代码优先还是数据库优先的方法,使用此方法都有些棘手,并且存在一些缺点。在这些情况下,您必须使用代码生成器生成的模型类的“部分性”和 MetadataTypeAttribute 来为字段分配 DisplayName。 

例如,假设您有一个名为“Student”的模型类,其中包含“FName”和“LName”字段;要为这些字段添加 DisplayName 属性,您必须将以下代码添加到文件中 

[MetadataTypeAttribute(typeof(Student.StudentMetadata))]
public partial class Student
{
    internal sealed class StudentMetadata
    {
        [DisplayName("First Name")]
        public string FName {get; set;}
 
        [DisplayName("Last Name")]
        public string LName {get; set;}
    }
}   

正如您所见,这种方法非常耗时,而且由于模型中的几乎任何更改都应该更新相应的类,因此也非常难以维护。

DisplayNameMetadataProvider 如何工作?

我不想详细介绍 ModelMetadataProvider 及其子类如何工作以收集模型中每个字段的元数据信息。如果您想进一步研究此主题,我推荐这篇文章

子类如何工作,但简而言之,我应该说这些类由 MVC 使用。我选择 DataAnnotationsModelMetadataProvider 作为基类,因为 ModelMetadataProvider 及其后代 AssociatedMetadataProvider 的所有抽象方法都由它实现,我只需要重写 CreateMetadata 方法。

DataAnnotationsModelMetadataProvider 或其后代之一被设置为 MVC 的默认模型元数据提供程序时,每次第一次使用模型字段时,都会调用 CreateMetadata。作为回报,字段的元数据将作为 ModelMetadata 类的实例返回,该类具有一个属性对应于每个标准的 DataAnnotation 属性。以下代码片段显示了 DisplayNameMetadataProviderCreateMetadata 方法。

protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
{
    ModelMetadata metadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
 
    if (containerType == null)
        return metadata;
 
    TypeDisplayNameData displayNameMetaData = null;
    if (attributes.OfType<DisplayNameAttribute>().Count() == 0)
    {
        displayNameMetaData = GetTypeDisplayNameData(containerType);
        if (displayNameMetaData != null)
        {
            string displayNameValue = String.Empty;
            if (displayNameMetaData.DisplayNames.TryGetValue(propertyName, out displayNameValue))
                metadata.DisplayName = displayNameValue;        
        }
    }
    return metadata;
}
在此方法的首行,它会调用其原始对应方法并获取由 propertyName 参数指定的模型字段的默认元数据。然后检查编译时是否已为其分配了 DisplayName 属性。如果没有,它将调用 GetTypeDisplayNameData 来获取在数据库中分配给该字段的容器类的所有显示名称,然后搜索这些名称以查找该字段的显示名称。如果它能找到该字段的显示名称,则将其分配给结果的 DisplayName 属性。如果 CreateMetadata 未能找到给定字段的任何显示名称,则将返回 DataAnnotationsModelMetadataProvider 的默认结果。

GetTypeDisplayNameData 方法

如前所述,GetTypeDisplayNameData 负责提供每个模型类的可用显示名称。调用时,此方法首先查找名为 FetchedDisplayNameData 的字典,以检查是否已提取给定类的显示名称。如果已提取,GetTypeDisplayNameData 将在不访问数据库的情况下返回显示名称。但如果尚未提取,此方法将调用 FetchTypeDisplayNameData,后者将实际访问数据库,读取分配给给定类的所有显示名称并返回它们。FetchTypeDisplayNameData 返回的结果将添加到 FetchedDisplayNameData 字典中供下次使用,然后返回给 CreateMetadata

FetchTypeDisplayNameData 方法的代码如下所示。我应该指出,在此方法的首四行中,该方法检查给定的类是否在我的实体框架数据模型(由 Models.ContactsEntities 表示)中,并且只有当它在数据模型中时,该方法才会尝试从数据库中获取该类的显示名称。

private SortedDictionary<string, string> FetchTypeDisplayNameData(Type containerType)
{
    Models.ContactsEntities context = new Models.ContactsEntities();
    System.Data.Metadata.Edm.MetadataWorkspace workspace = ((System.Data.Entity.Infrastructure.IObjectContextAdapter)context).ObjectContext.MetadataWorkspace;
    System.Collections.ObjectModel.ReadOnlyCollection<System.Data.Metadata.Edm.GlobalItem> items = workspace.GetItems(System.Data.Metadata.Edm.DataSpace.OSpace);
    if (items.OfType<System.Data.Metadata.Edm.EntityType>().Any(x => x.FullName == containerType.FullName))
    {
        var displayNames =
            from displayName in context.FieldDisplayNames
            where displayName.Namespace == containerType.Namespace
                && displayName.ClassName == containerType.Name
            select displayName;
        if (displayNames.Count() == 0)
            return null;
        else
        {
            var fieldDisplayNameDictionary = displayNames.ToDictionary<TestWebApp.Models.FieldDisplayName, string, string>(x => x.FieldName, x => x.DisplayName);
            return new SortedDictionary<string, string>(fieldDisplayNameDictionary);
        }
    }
    else
        return null;
}

DisplayNameMetadataProvider 的数据库结构

FetchTypeDisplayNameData 方法所示,显示名称存储在名为 FieldDisplayNames 的表中。此表的结构如下面的图所示,是一个实体框架模型。

使用代码

本文附带了一个使用此提供程序的简单项目。该项目只有一个实体,Contact,它存储有关用户联系人的某些信息。正如您所看到的,有四个视图与此实体一起工作,并且它们都使用 Html.LabelFor 方法来显示其字段的标签。您可以更改 FieldDisplayName 表中 Contact 实体的字段标签;您需要记住,由于 MVC 只创建一次每个字段的元数据,因此在每次更改后都需要重新启动 Web 服务器。

要使用 DisplayNameMetadataProvider,您必须将其添加到您的项目中,并将 Models.ContactsEntities 更改为您自己的数据库上下文。然后,您还必须将 FieldDisplayName 表添加到您的数据库中。最后,您应该告诉 MVC 将此提供程序用作其默认提供程序;为此,您必须在 Global.asax 文件中的 Application_Start 方法的末尾添加以下行:

ModelMetadataProviders.Current = new DisplayNameMetadataProvider();

可能的改进

DisplayNameMetadataProvider 可以进行一些可能的改进,使其更通用、更易于使用,其中一些是

  1. 使 DisplayNameMetadataProvider 通用化,使其不依赖于类型的数据库上下文
  2. 将其封装在类库中,以便更好地重用
  3. DisplayNameMetadataProvider 在本地化应用程序中具有巨大的应用潜力;通过对其及其数据结构进行一些简单的更改,DisplayNameMetadataProvider 可以轻松地用于此类应用程序。
© . All rights reserved.