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

Entity Framework 4.1 POCO 类生成模板(带通知功能)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.60/5 (5投票s)

2011年10月19日

CPOL

13分钟阅读

viewsIcon

24532

downloadIcon

142

向 Microsoft 的 VB.NET 文本转换文件添加了属性更改通知和验证代码,这些文件用于创建 POCO 类和 DBContext 类。

目录

背景

默认情况下,Entity Framework 中的所有实体类都继承自 EntityObject 类。这使得它们依赖于 EF。在 4.1 版本中,Microsoft 提供了一种创建持久化无关类的方法:Plain Old CLR Objects (POCO)。这些对象仅在附加到实体容器时才了解持久化。这是通过动态派生自实体类的代理类来实现的。Microsoft 没有隐藏这些类在模型设计器中的生成,而是提供了文本转换文件。因此,如果您不喜欢 Microsoft 生成实体类的方式,只需更改转换文件!我就是这么做的。我还创建了概念模型注解属性,让您可以选择性地开启或关闭我的代码生成。然后,我将所有这些整合到一个项目项模板中,这样您就可以直接添加我的模板而不是 Microsoft 的。Microsoft 同时提供了 C# 版本和 VB 版本。我只制作了 VB 版本。

引言

我的改动

以下是我所做更改的列表。

  • 所有属性都是 virtual(在 VB 中是 overridable)。这使得实体支持自动跟踪更改。
  • 概念模型中的文档元素被复制到 XML 帮助文本。Summary 映射到 SummaryLongDescription 映射到 Remarks
  • 在属性更改之前,代码会调用一个名为 pOn[PropertyName]Changing 的部分方法,您可以在其中进行属性验证。
  • 在属性更改之后,代码会调用一个方法来引发 PropertyChanged 事件以进行 WPF 通知。
  • 同样,在更改之后,代码会调用一个名为 pOn[PropertyName]Changed 的部分方法,您可以在其中进行进一步的更改通知。
  • 创建公共常量来保存属性特征 MaxLengthPrecisionScale 的值(仅当属性具有这些特征时)。
  • 在实体更新或插入数据库之前,代码会调用一个名为 pValidate[EntityName] 的部分方法,您可以在其中进行实体级别的验证。

必需的功能

在属性级别进行通知和验证并不像看起来那么直接。您并非总是希望这样做。以下是一些您不希望进行通知和验证的情况:

  1. 当实体正在从数据库加载时。
  2. 当您创建一个新的实体对象并初始化某些属性时。
  3. 但您确实希望在将新实体对象添加到实体集后进行通知。
  4. 当您取消待处理的更新时。您仍然希望通知更改,但不验证它们。
  5. 当您执行批量更新时。这是出于性能原因。您可能仍然需要根据对批量代码的信任程度来验证。

必需的代码

为了支持所需功能并引发属性更改事件,我要求所有实体类和复杂类都继承 EntityBase 类。此类必须包含以下三个成员:

  • 一个公共布尔属性 IsNotifyEnabled
  • 一个公共布尔属性 IsValidationEnabled
  • 一个名为 pRaisePropertyChanged 的方法,它将以以下签名引发 PropertyChanged 事件:
  • Protected Sub pRaisePropertyChanged( args As PropertyChangedEventArgs) 

模板中包含一个示例类。此类的代码不是生成的,只是复制过来的。因此,您可以修改它,或者如果您已经有一个能够支持这些成员的类,则可以删除它。如果您不喜欢 EntityBase 这个名字,可以更改两个文本转换文件 "Types.tt" 和 "Container.tt"。在每个文件的开头附近,您会找到:

Dim entityBaseName As String  = "EntityBase"

如果您不喜欢成员名称,可以进行“全部替换”。如果您不熟悉这些文本转换文件,一个很好的入门是 Microsoft 数据开发中心的 T4 模板和 Entity Framework(^)

生成的代码概览

非复杂属性的生成代码

名为 Title 的字符串属性的 setter 代码如下所示:

Set(ByVal value As String)
  Dim oldValue As String
  Dim newValue As String
  If pIsTitleInitialized Then
      If pTitle Is value Then Exit Property
      oldValue = pTitle
      newValue = value
      If IsValidationEnabled Then
          pOnTitleChanging(oldValue, newValue)
      End If
      pTitle = newValue
      If IsNotifyEnabled Then
          pRaisePropertyChanged(pTitlePropertyArgs)
          pOnTitleChanged(oldValue, newValue)
      End If
  Else
      pIsTitleInitialized = True
      pTitle = value
  End If
End Set

这段代码的开头会检查字段 pIsTitleInitialized。它被初始化为 false。如果您查看 "Else" 下面的底部,您会发现它会开启这个设置并保存值。因此,如果属性尚未初始化,则不会进行通知或验证。这满足了我们 必需的功能 1 和 2(在从数据库加载或初始化新实体时不做任何额外操作)。

一旦我们确定已初始化,我们执行:

pTitle Is value Then Exit Property

如果没有任何更改,则退出。这段代码的生成取决于属性的类型。字符串和实体引用使用 "Is"。所有其他非可空类型使用 "="。可空类型则有所不同。例如,名为 "PubYear"、类型为 "Integer" 的属性将生成:

If pPubYear = value OrElse
(pPubYear Is Nothing AndAlso value Is Nothing) Then Exit Property

一旦我们确定属性确实会发生更改,我们就检查 "IsValidationEnabled" 和之后的 "IsNotifyEnabled"。这些是来自 EntityBase 的属性。正如您所见,如果我们将它们设置为 false,我们可以停止验证和/或通知。这满足了我们的 必需的功能 4 和 5。

这样我们就剩下要求 3 了。我们需要一种方法来确保在将新实体添加到其实体集之前所有属性都已初始化。为了实现这一点,实体类和复杂类会获得一个方法,该方法会设置每个属性的 pIs[PropertyName]Initialized 字段或调用复杂属性的方法。因此,在添加新实体之前,我们只需执行 newEntity.SetInitialized(True)

关于复杂属性

复杂属性永远不会被验证。它们所引用的对象中的属性会被验证。复杂属性本身不应被更改。更改的是它们所引用的对象中的属性。但是,如果复杂类重写了 ToString 并提供有意义的表示,则可以显示复杂属性。您甚至可以使用一个作为排序列,如果该类实现了 IComparable。因此,我们可能希望在所引用的对象中的任何属性发生更改时收到通知。生成的代码会这样做,但有一个假设:每个复杂对象只存在于一个对象的唯一一个属性中。如果您不更改复杂属性,这应该是真的。通过做出这个假设,我们可以说复杂属性“拥有”它的复杂对象。

复杂类的额外生成代码

复杂类会获得额外的代码来处理通知其“所有者”。

'-------- Owner Notification --------
Friend Property  RaiseOwnerPropertyChanged As Action = Sub() Exit Sub
Private Sub pRaiseMineAndOwnerPropertyChanged(
                 arg As PropertyChangedEventArgs)
                          pRaisePropertyChanged(arg)
                          RaiseOwnerPropertyChanged.Invoke()
End Sub

当实体类中的属性需要引发属性更改事件时,它们会调用 EntityBase 方法 pRaisePropertyChanged。复杂类中的属性则有所不同:它们调用此例程。然后,该例程会调用 EntityBase 方法,然后调用 RaiseOwnerPropertyChanged 操作,如果所有者希望收到通知,则会设置该操作。如果不想,该操作默认是一个什么都不做的 "Exit Sub"。

复杂属性的生成代码

实体 "Book" 中类型为 "FullName" 的复杂属性 "AuthorName" 的代码如下所示:

'-------- AuthorName --------
Public Overridable Property AuthorName As FullName
  Get
      If pAuthorName Is Nothing Then
          pAuthorName = New FullName
          pAuthorName.RaiseOwnerPropertyChanged = 
              Sub()
                  pRaisePropertyChanged(pAuthorNamePropertyArgs)
                  pOnAuthorNameChanged(pAuthorName)
              End Sub
      End If
      Return pAuthorName
  End Get
  Protected Set(ByVal value As FullName)
      pAuthorName = value
      pAuthorName.RaiseOwnerPropertyChanged = 
              Sub()
                  pRaisePropertyChanged(pAuthorNamePropertyArgs)
                  pOnAuthorNameChanged(pAuthorName)
              End Sub
  End Set
End Property

Private pAuthorName As FullName
Private Shared ReadOnly pAuthorNamePropertyArgs As New _
        PropertyChangedEventArgs("AuthorName")

Partial Private Sub pOnAuthorNameChanged(complexObject As FullName)
End Sub

关于代码的一些说明

  • Setter 是 "Protected"。这是我们能接近“只读”的程度,因为我们必须允许实体代理访问。“Protected”是在实体模型中指定的。默认情况下,除非 setter 是 Protected,否则不会生成复杂属性的通知代码。
  • Getter 会在复杂对象不存在时创建一个新的复杂对象。即使通知代码未生成,也会发生这种情况。
  • Getter 和 Setter 都会将复杂对象的 RaiseOwnerPropertyChanged 操作设置为调用我们的 EntityBase 方法,然后调用一个您可以选择性编写以进行进一步通知逻辑的部分方法。

实体验证的生成代码

Protected Overrides Function ValidateEntity(
            entityEntry As DbEntityEntry,
            items As IDictionary(Of Object, Object)
            ) As DbEntityValidationResult

  Dim mErrors = New List(Of DbValidationError)()
  If DirectCast(entityEntry.Entity, EntityBase).IsValidationEnabled Then
      Select Case entityEntry.Entity.GetType.BaseType
          Case GetType(Book)
              pValidateBook(entityEntry.Cast(Of Book), mErrors)
          . . . .
          . . . .
      End Select
  End If
  Dim mResult = New DbEntityValidationResult(entityEntry, mErrors)
  Return mResult
End Function

关于代码的一些说明

  • 实体被强制转换为 EntityBase,以便能够检查验证是否已启用。
  • Select 语句获取实体的类型,然后获取其基类型。这样做是因为要验证的实体是代理实体,而我们想要的是“真实”实体类型。

如何验证

如何进行属性验证

我们以实体 "Book" 中名为 "Title" 的字符串属性为例。(也可以是复杂类中的属性。过程相同。)首先,您需要创建一个名为 "Book" 的类来配合生成的局部类。接下来,在此类中,您需要创建一个方法:

Private Sub  pOnTitleChanging(
                  ByVal oldValue As String,
                  ByRef newValue As String)

此方法会重写在局部类中生成的部分方法。您可以让 VB 为您生成此方法。如果您在 VB 编辑器右上角的下拉列表中,您会看到所有部分方法都是灰色的。只需单击您想要的方法,VB 就会为您插入它。

当发现无效内容时,您的代码必须抛出异常。为了让 WPF 知道这一点,您的 XAML 代码可以如下所示:

<TextBox Text="{Binding Title, 
            ValidatesOnExceptions=True, 
            NotifyOnValidationError=True}" /> 

有时,修复无效内容比打扰用户更好。您可以通过简单地更改 newValue 参数来做到这一点。您会注意到它被传递为 ByRef 而不是 ByVal

由于这是一个字符串属性,您可以使用从概念模型生成的常量 TitleMaxLength。如果这是一个 decimal 属性,您将拥有常量 [PropertyName]Precision[PropertyName]Scale

请记住,这个类是一个持久化无关的类。这意味着您唯一可以访问的数据是此实例中的数据以及通过导航属性可访问的任何实体对象。例如,您无法检查实体集是否存在重复项。要做到这一点,您需要进行实体级别的验证。

如何进行实体验证

实体验证发生在实体容器中,而不是在实体类型中。这是所有持久化发生的地方。这个类的名称在您的概念模型中定义为“实体容器名称”属性。与实体类一样,您需要创建自己的类来匹配生成的局部类。为了验证我们在属性验证中使用的 "Book" 实体,您需要将此方法添加到您的实体容器类中:

Private Sub pValidateBook(
                entityEntry As DbEntityEntry(Of Book), 
                validationErrors As ICollection(Of DbValidationError)) 

请注意,entityEntry 参数是特定于 "Book" 实体的。这是 Entity Framework 用于跟踪更改的对象。通过它可以找出验证是由于修改还是添加(删除不进行验证)。您还可以检查 "Book" 中的特定属性是否已修改。如果发现无效内容,则将错误对象添加到 validationErrors 参数中。

此例程从 SaveChanges 调用。如果发现 validationErrors 参数不为空,它将抛出一个 DbEntityValidationException,其中包含所有经过验证的实体的所有错误消息。您也可以通过调用 GetValidationErrors 来调用实体验证。

控制生成内容

什么是注解?

Microsoft 允许您使用额外的属性和元素来注解您的概念模型。我利用了这一点,定义了两个布尔属性:GenerateEntityValidationGeneratePropertyNotify。这些允许您为每个实体类型和属性选择性地开启或关闭生成额外的代码。

GenerateEntityValidation 属性

默认情况下,实体验证的部分方法会为所有非抽象实体类型(VB 中是 MustInherit)生成。您可以通过将此属性设置为 "false" 来为特定实体类型停止此操作。如果您将此属性设置在实体容器上并设置为 "false",那么验证方法将仅为具有此属性设置为 "true" 且非抽象的实体生成。

GeneratePropertyNotify 属性

默认情况下,通知和验证代码会为所有实体类型和复杂类型中的所有属性和导航属性生成,但以下情况除外:

  • 具有主导角色的导航属性(实体集合属性)。
  • (主)键属性。
  • 外键属性。
  • 可更新的复杂属性(setter 访问权限为 publicinternal)。
  • "只读"的非复杂属性(setter 访问权限为 privateprotected)。

要为默认情况下不生成额外代码的属性生成额外代码,请将该属性(导航实体集合属性除外)设置为 "true"。或者,要关闭某个属性的额外代码生成,请将其设置为 "false"。如果您将此属性在实体类型或复杂类型上设置为 "false",那么该类型中具有额外生成代码的属性将仅限于那些此属性设置为 "true" 的属性。

如何注解你的概念模型

要注解您的概念模型,您需要使用 XML 编辑器而不是设计编辑器来打开您的实体模型。要做到这一点:

  • 右键单击您的实体模型文件(.edmx
  • 选择“打开方式...”
  • 选择“XML (文本)编辑器”

您将看到的第一个架构是存储架构。如果将其折叠,您会看到概念架构,它看起来像这样:

<!-- CSDL content -->
<edmx:ConceptualModels>
  <Schema Namespace="YourModel" Alias="Self"
          xmlns:annotation="http://schemas.microsoft.com/ado/2009/02/edm/annotation" 
          xmlns=http://schemas.microsoft.com/ado/2008/09/edm>

您需要向 schema 元素添加一个 XML 命名空间:

xmlns:code="http://EntityModel.Annotations.Notify"

您现在将获得我定义的属性的 IntelliSense 帮助,位于 "code:" 下。这是因为我的模板添加了 XML 架构文件 [ModelFileName].Annotations.Notify.xsd

模板

先决条件

此模板至少需要 Entity Framework 4.1。如果您尚未安装,可以在 Microsoft 下载中心(^) 获取。

模板添加的文件

模板会添加四个文件。每个文件的名称都以您的模型文件名(.edmx 文件)为前缀:

  • [ModelFileName].Annotations.Notify.xsd
  • 这是我的注解属性的架构。当您注解概念模型时,它会提供 IntelliSense。

  • [ModelFileName].Container.tt
  • 这是生成局部实体容器类的文本转换文件。此类继承自新的 4.1 DBContext 类,而不是 ObjectContext

  • [ModelFileName].EntityBase.vb
  • 这是示例 EntityBase 类。请参阅 必需的代码

  • [ModelFileName].Types.tt
  • 这是生成所有实体和复杂局部类的文本转换文件。

安装模板

要安装模板,请下载模板(9.62 KB)并将其复制到您的文件夹:“Visual Studio 2010 /Templates /Item Templates /Visual Basic /Code”。您可能需要创建最后一个文件夹。不要解压。模板文件是 zip 文件。

使用模板

如果您仍然使用默认的代码生成策略(即,您的实体继承自 EntityObject),则需要先禁用它:

  • 在设计模式下打开您的实体模型。
  • 右键单击空白区域,然后选择“属性”。
  • “属性”中的第一项应该是“代码生成策略”。将其设置为“无”。

如果您已经在使用 Microsoft 的模板,则需要删除他们生成的 C# 文件:

  • [ModelFileName].Context.tt
  • [ModelFileName].tt

如果您跳过这些步骤,您将得到重复的类。您仍然可以在之后执行这些步骤,但先执行可以避免所有这些错误。

现在您可以添加新模板了。

  • 右键单击包含您的实体模型(.edmx)的文件夹,然后选择“添加”/“新建项...”
  • 在“已安装模板”下,展开“常用项”,然后选择“代码”。
  • 您应该会看到新模板,标题为“ADO.NET DbContext Generator with Notify”。点击它。
  • 如果查看列表右侧,您会看到它提示您此项的名称必须与您的实体模型文件相同。
  • 一旦您将其命名正确,请单击“添加”。
  • 将添加四个文件,并生成所有类。这可能需要几秒钟,具体取决于您的类数量。

如果您使用过 Microsoft 的模板,您知道需要从模型的上下文菜单中选择“添加代码生成项...”来选择它。如果您这样做,您将找不到我的模板。我找不到如何将其添加到此选择组中。因此,我必须依赖您正确拼写该项。如果您没有这样做,您将收到一些糟糕的错误,其中提到找不到文件。

历史

  1. 发布于 2011 年 10 月 19 日。
© . All rights reserved.