Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / VB

Template for generating POCO classes with Notify in Entity Framework 4.1

4.60/5 (5 votes)
18 Oct 2011CPOL13 min read 24K   142  
Added property change notification and validation code to Microsoft’s VB.NET text transform files that create the POCO classes and DBConext class.

Contents

Background

By default, all entity classes in Entity Framework are generated inheriting the EntityObject class. This makes them dependent on EF. With release 4.1, Microsoft provided a way of creating persistence ignorant classes: just Plain Old CLR Objects (POCO). These objects learn about persistence only when they are attached to an entity container. This is done through the magic of proxy classes that are dynamically derived from the entity classes. Microsoft did not hide the generation of these classes in the model designer, but instead provided text transform files. So if you don’t like the way Microsoft generated the entity classes, just change the transform files! That’s what I did. I also created conceptual schema annotation attributes that let you selectively turn on or off my code generation. Then I put this all together in a project item template so that you can just add my template instead of the Microsoft one. Microsoft provides both a C# version and a VB version. I only made a VB version.

Introduction

What I Changed

Following is a list of the changes I made.

  • All properties are virtual (overridable in VB speak). This makes the entities support automatic change tracking.
  • Documentation elements in the conceptual schema are copied to XML help text. Summary to Summary and LongDescription to Remarks.
  • Before a property changes, the code invokes a partial method pOn[PropertyName]Changing where you can do your property validation.
  • After a property changes, the code invokes a method that will raise the PropertyChanged event for WPF notification.
  • Also after change, the code invokes a partial method pOn[PropertyName]Changed where you can do further change notification.
  • Public constants are created to hold the value of the property facets MaxLength, Precision, or Scale (only if the property has them).
  • Before an entity updates the database or is inserted into the database, the code invokes a partial method pValidate[EntityName] where you can do entity level validation.

Required features

Notifying and validating at the property level is not as straightforward as it might seem. You don’t always want to do them. Following is a list of cases when you don’t:

  1. When the entities are being loaded from the database.
  2. When you create a new entity object and initialize some of the properties.
  3. But you do want to, after you add the new entity object to the entity set.
  4. When you cancel a pending update. You still want to notify the changes but not validate them.
  5. When you do batch updates. This is for performance reasons. You may still want to validate depending on how well you trust the batch code.

Required code

In order to support the required features and raise the property changed event, I require all entity and complex classes to inherit the EntityBase class. This class must have these three members:

  • A public Boolean property IsNotifyEnabled
  • A public Boolean property IsValidationEnabled
  • A method named pRaisePropertyChanged that will raise the PropertyChanged event with the following signature:
  • VB
    Protected Sub pRaisePropertyChanged( args As PropertyChangedEventArgs) 

A sample class is included in the template. This class is not generated. It’s just copied. So you can modify it or delete it if you already have a class that can support these members. If you don’t like the name EntityBase, you can change the two text transform files "Types.tt" and "Container.tt". Somewhere close to the beginning of each file, you will find:

VB
Dim entityBaseName As String  = "EntityBase"

If you don’t like the member names, you can do a "replace all". If you are unfamiliar with these text transform files, a good introduction is T4 Templates and the Entity Framework(^) from Microsoft's Data Development Center.

A look at the generated code

Non-complex property generated code

The setter code for the string property named Title will look like this:

VB
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

The beginning of this code checks the field pIsTitleInitialized. This is initialized to false. If you look near the bottom after the "Else", you will see that it turns this on and saves the value. So, if the property is not initialized, it won’t notify or validate. This satisfies our Required features 1 and 2 (don’t do anything extra when loading from database or initializing a new entity).

Once we decide that we’re initialized, we do:

VB
pTitle Is value Then Exit Property

which exits if nothing changed. The code generated for this depends on the type of the property. Strings and entity references use "Is". All other non-nullable types use "=". Nullable types do something different. For example, a property named "PubYear" with type "Integer" will generate:

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

Once we decide that the property will actually change, we check "IsValidationEnabled" and later on "IsNotifyEnabled". These are the properties from EntityBase. As you can see, if we set these to false, we can stop validation and/or notification. This satisfies our Required features 4 and 5.

That leaves us with requirement 3. We need a way to insure all properties are initialized before we add a new entity to its entity set. To accomplish this, entity and complex classes get a method which will set each property’s pIs[PropertyName]Initialized field or call the complex property’s method. So, all we have to do before adding a new entity is newEntity.SetInitialized(True).

About complex properties

Complex properties are never validated. The properties in the object they reference are validated. Complex properties shouldn’t be changed. The properties in the object they reference are changed. But a complex property can be displayed if the complex class overrides ToString with something meaningful. You can even use one as a sort column if the class implements IComparable. So we might want to be notified if any of the properties in the referenced object changes. The generated code will do this, but it makes one assumption: that each complex object exists in one and only one property in one and only one object. This should be true if you don’t change a complex property. By making this assumption, we can say that a complex property "owns" its complex object.

Complex class extra generated code

A complex class gets extra code to handle notifying its "owner".

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

When properties in entity classes want to raise the property changed event, they call the EntityBase method pRaisePropertyChanged. Properties in complex classes do something different: they call this routine. This then calls the EntityBase method and then invokes the action RaiseOwnerPropertyChanged, which will be set by the property owner if it wants to be notified. If it does not, the action defaults to a do-nothing "Exit Sub".

Complex property generated code

The code for the complex property "AuthorName" of type "FullName" in entity "Book" looks like this:

VB
'-------- 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

Some notes on the code:

  • The setter is "Protected". This is as close to "read only" as we can get because we must allow the entity proxies access. "Protected" was specified in the entity model. By default, notification code for complex properties won’t be generated unless the setter is Protected.
  • The getter creates a new complex object if one does not exist. This happens even if the notification code is not generated.
  • Both the getter and setter set the complex object’s RaiseOwnerPropertyChanged action to call our EntityBase method and then call a partial method that you can optionally code for further notification logic.

Entity validation generated code

VB
Protected Overrides Function ValidateEntity(
            entityEntry As DbEntityEntry,
            items As IDictionary(Of ObjectObject)
            ) 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

Some notes on the code:

  • The entity is cast to EntityBase so that it can check if validation is enabled.
  • The Select statement gets the type of the entity and then gets its base type. It does this because the entity to be validated is a proxy entity and we want the "real" entity type.

How to validate

How to do property validation

Let’s take an example of a string property named "Title" in an entity named "Book". (Or it could be a property in a complex class. The process is the same.) You first create a class named "Book" to go with the generated partial class. Next, within this class, you create a method:

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

This overrides the partial method generated in the partial class. You can let VB generate this for you. If you pull down the list at the top right of the VB editor, you will see all of the partial methods grayed out. Just click the one you want, and VB will insert it for you.

Your code must throw an exception when it finds something not valid. To let WPF know about this, your XAML code can look like this:

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

Sometimes it’s better to fix something invalid rather than pester the user. This you can do by just changing the parameter newValue. You will notice that it is passed ByRef instead of ByVal.

Since this is a string property, you have access to the constant TitleMaxLength, which was generated from the conceptual model. If this were a decimal property, you would have constants [PropertyName]Precision and [PropertyName]Scale.

Remember that this class is a persistence ignorant class. This means that the only data you have access to is the data in this instance and any entity object accessible through the navigation properties. For example, you cannot check for duplicates against an entity set. To do that, you need entity level validation.

How to do entity validation

Entity validation happens in the entity container rather than in the entity type. This is where all the persistence happens. The name of this class is defined in your conceptual model with the property "Entity Container Name". Like an entity class, you have to create your own to match the generated partial class. To validate our "Book" entity that we used in property validation, you add this method to your entity container class:

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

Note that the entityEntry parameter is specific to "Book" entities. This is the object that Entity Framework uses to keep track of changes. From this, you can find out whether the validation is due to modification or addition (deletes aren’t validated). You can also check whether a specific property within "Book" was modified. If you find something invalid, you add an error object to the parameter validationErrors.

This routine gets invoked from SaveChanges. If it finds the parameter validationErrors is not empty, it will throw a DbEntityValidationException, which contains all the error messages from all the validated entities. You can also invoke entity validation by calling GetValidationErrors.

Controlling what gets generated

What’s Annotation?

Microsoft allows you to annotate your conceptual schema with additional attributes and elements. I took advantage of this by defining two boolean attributes: GenerateEntityValidation and GeneratePropertyNotify. These let you turn on or off generating the extra code by entity type and property.

GenerateEntityValidation attribute

By default, the partial method for entity validation is generated for all entity types that are not abstract (MustInherit in VB speak). You can stop this for a particular entity type by setting this attribute to "false". If you set this attribute on the entity container to "false", then the validation method is generated only for those with this attribute set to "true" and not abstract.

GeneratePropertyNotify attribute

By default, the notification and validation code is generated for all properties and navigation properties in all entity types and complex types, except the following:

  • Navigation properties with the principal role (an entity collection property).
  • (Primary) key properties.
  • Foreign key properties.
  • Updatable complex properties (setter access of public or internal).
  • "Read only" non-complex properties (setter access of private or protected).

To have the extra code generated for a property that it does not do by default, set the attribute to "true" on that property (except navigation entity collection properties). Or to turn off extra code generation for a property, set it to "false". If you set this attribute to "false" on an entity type or complex type, then the only properties within that type with extra generated code are those with this attribute set to "true".

How to annotate your conceptual schema

To annotate your conceptual schema, you need to open you entity model with the XML editor instead of the design editor. To do this:

  • Right click your entity model file (.edmx)
  • Pick "Open With …"
  • Pick "XML (Text) Editor"

The first schema you will see is the storage schema. If you collapse this, you see the conceptual schema that looks like this:

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>

You need to add an XML namespace to the schema element:

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

You will now get IntelliSense help for my attributes under "code:". This happen because my template added the XML schema file [ModelFileName].Annotations.Notify.xsd.

The template

Prerequisite

This template requires at least 4.1 of Entity Framework. If you don't have it installed, you can get it at Microsoft's Download Center(^).

The files added by the template

Four files get added by the template. Each file name is prefaced by your model file name (the .edmx file):

  • [ModelFileName].Annotations.Notify.xsd
  • This is the schema of my annotation attributes. It provides IntelliSense when you annotate your conceptual schema.

  • [ModelFileName].Container.tt
  • This is the text transform file that generates the partial entity container class. This class inherits from the new 4.1 DBContext class rather than ObjectContext.

  • [ModelFileName].EntityBase.vb
  • This is the sample EntityBase class. See required code.

  • [ModelFileName].Types.tt
  • This is the text transform file that generates all of the entity and complex partial classes.

Installing the template

To install the template, download the template (9.62 KB) and copy it to your folder: "Visual Studio 2010 /Templates /Item Templates /Visual Basic /Code". You may have to create the last folder. Don't unzip it. Template files are zip files.

Using the template

If you are still using the default code generation strategy (that is, your entities are inheriting from EntityObject), then you first need to turn this off:

  • Open your entity model in design mode.
  • Right click on some blank space and pick Properties.
  • The first item in Properties should be "Code Generation Strategy". Set this to "none".

If you are already using Microsoft's template, you need to delete their generated files:

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

If you skip these first steps, you wind up with duplicate classes. You can still do the steps after, but doing them first avoids all of those errors.

Now you're ready to add the new template:

  • Right click the folder that contains your entity model (.edmx) and pick Add /New Item...
  • Under "Installed Templates", expand "Common Items" and pick "Code".
  • You should see the new template as the first one titled "ADO.NET DbContext Generator with Notify". Click it.
  • If you look to the right of the list, you will see that it's telling you that the name of this item must be the same as your entity model file.
  • Once you have named it correctly, click Add.
  • The four files will be added and all of the classes generated. This may take a few seconds, depending on how many classes you have.

If you have used Microsoft's template, you know that you pick theirs from the model's context menu "Add Code Generation Item ...". If you do that, you won't find mine. I couldn't figure out how to get in this select group. So I have to depend on you spelling the item correctly. If you don't, you will get some nasty errors complaining, among other things, that it can't find the file.

History

  1. Posted October 19, 2011.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)