Monday, January 07, 2008

Partial classes and methods in VB.NET

For the current project I'm on we decided to spend a little time upgrading our code generation tool so that we could generate DAL classes more easily from our database tables. Part of this involved adding in extra loops to make the generated code bulletproof so we could rely on what was generated and not need to think twice about re-generating classes when we wanted to fix a bug or add a new piece of functionality.

To be clear, the generator would create seven files:
  1. Basic entity class (e.g. Group.vb)
  2. Collection class (GroupCollection.vb)
  3. A DAL class (e.g. GroupDAL.vb)
  4. Insert stored procedure
  5. Update stored procedure
  6. Delete stored procedure
  7. Select stored procedure to get a single record by primary key(s)
[Disclaimer: This is a legacy system, so field and variable names in sample code below are specified for me, and don't reflect my own thoughts on naming conventions.]

The first step was to add more smarts to the templates so that the generator could handle some difficult situations (multiple primary keys, non-identity single primary keys) and to add in some additional base functionality (collection sorting, object caching, OnValidate checking that the IList interface is not being abused).

For a good article on strongly typed Collection classes check out this one on Builder AU.

Next we looked at the range of customisation we already had in existing data layer classes and ways of separating this out so that we could add ways of getting data that meet specific business requirements without polluting our generated classes. The answer of course was to use partial classes.

“we nearly always need an additional partial DAL class to hold useful extra ways of retrieving data”
By declaring that our generated classes were partial public classes we could complete the rest of the class in another file that is not being auto-generated. Somehow I ended up christening these for the team, and where necessary we now have additional files (e.g. Group_Additional.vb, GroupCollection_Additional.vb, GroupDAL_Additional.vb). It turns out that we nearly always need an additional partial DAL class to hold useful extra ways of retrieving data (e.g. searches, find by foreign key, etc.), with regards to collections and entity classes it was less useful, but more on that later. We now had a way to bring in our legacy customisations, whilst still generating most of the code.

Our DAL classes now start like this:

    1 Imports System

    2 Imports System.Data.SqlClient

    3 Imports System.Data

    4 Imports System.Text

    5 Imports System.Collections

    6 Imports Elcom.Common.Data


    8 ''' <summary>

    9 ''' Data Access layer (DAL) class for Group entity objects.

   10 ''' </summary>

   11 ''' <remarks>

   12 ''' Auto-generated by Elcom Code Generator at 4/01/2008 4:01:03 PM.

   13 ''' </remarks>

   14 Partial Public Class GroupDAL

   15     Inherits BaseDAL


   17     ''' <summary>

   18     ''' Constructor method is declared and made private to ensure

   19     ''' class is a singleton.

   20     ''' </summary>

   21     Private  Sub New()

   22     End Sub


   24     ''' <summary>

   25     ''' Public 'Instance' method allows external objects to access

   26     ''' the current singleton instance.

   27     ''' </summary>

   28     ''' <returns>

   29     ''' Returns a reference to the single GroupDAL object

   30     ''' in memory.

   31     ''' </returns>

   32     Public Shared ReadOnly Property Instance() As GroupDAL

   33         Get

   34             If _instance Is Nothing Then

   35                 SyncLock GetType(GroupDAL)

   36                     If _instance Is Nothing Then

   37                         _instance = new GroupDAL()

   38                     End If

   39                 End SyncLock

   40             End If

   41             Instance = _instance

   42         End Get

   43     End Property

The DAL additional class looks like this:

    1 Imports System

    2 Imports System.Data.SqlClient

    3 Imports System.Data

    4 Imports System.Text

    5 Imports System.Collections

    6 Imports Elcom.Common.Data


    8 ''' <summary>

    9 ''' Data Access Layer class for Group

   10 ''' </summary>

   11 ''' <remarks>

   12 ''' Old code brought in as Additional partial class by Angus

   13 ''' on 4/01/2008 for CM 5.5.

   14 ''' </remarks>

   15 Partial Public Class GroupDAL


Unfortunately we were still left with a major issue, some of the customisation we had made involved making changes to the way we saved some data items (e.g. encrypting users' passwords before saving them to the database), or having additional read-only data items that were not instantiated in the database table but properly belonged to the entity. The partial class solution allowed us to add extra properties, but did not enable us to override the basic Save() method, so what were we to do?

One option was to implement an alternative Save() method that differed either via parameter signature or name, but that would leave the incorrect one hanging around. Another trade off would be to customise the generated class file and just live with the necessity to not generate that file (problematic and prone to human error).

The answer turned out to be partial methods. The VB team blogged about these last year:
“Partial methods enable code generators to write extremely flexible code by creating a lot of "hooks", where developers can provide their own custom functionality that integrates into the "boiler plate" code created by the generator. Because the hooks are optimized away if they aren't used, no performance penalty is introduced by defining them. This is really useful if generated code needs to be used in high performance scenarios. For example, partial methods are used by the DLINQ designer for exactly this purpose. Developers wishing to invoke custom code on their data objects when properties are set can do so without requiring all users of DLINQ to suffer performance problems.”
So we ended up adding in calls to partial methods in all of our property setting code:


   15     Public Property intGroupID() As Int32

   16         Get

   17             Return _intGroupID

   18         End Get

   19         Set (ByVal value As Int32)

   20             ' partial event method to allow insertion of

   21             ' behaviour using partial class

   22             OnSettingintGroupID(value)           

   23             _intGroupID  = value

   24         End Set

   25     End Property


We also ended up adding pre/post save calls to partial methods to allow us to intervene and tweak either data to be saved or to handle other behaviour post the save operation. An example of a typical save operation is below:


   71     ''' <summary>

   72     ''' Public method allows external objects to save changes to a Group object.

   73     ''' </summary>

   74     Public Sub Save(ByRef objGroup As [Group], ByVal blnForUpdate As Boolean)

   75         Dim parameters As ArrayListNew ArrayList()

   76         Dim sproc As String


   78         parameters.Add(New SqlParameter("@strName",objGroup.strName))

   79         parameters.Add(New SqlParameter("@intURLID",objGroup.intURLID))

   80         parameters.Add(New SqlParameter("@blnAllowAddressUpdateInEshop",objGroup.blnAllowAddressUpdateInEshop))

   81         parameters.Add(New SqlParameter("@blnShowAdminInTopMenu",objGroup.blnShowAdminInTopMenu))

   82         parameters.Add(New SqlParameter("@strLDAPGroupName",objGroup.strLDAPGroupName))

   83         parameters.Add(New SqlParameter("@strLDAPIdentifier",objGroup.strLDAPIdentifier))

   84         parameters.Add(New SqlParameter("@blnSystemsAdministrator",objGroup.blnSystemsAdministrator))

   85         parameters.Add(New SqlParameter("@blnRestrictAccessBaseFol",objGroup.blnRestrictAccessBaseFol))

   86         parameters.Add(New SqlParameter("@blnRestrictArticleOptAttr",objGroup.blnRestrictArticleOptAttr))

   87         parameters.Add(New SqlParameter("@blnRestrictFolderOptAttr",objGroup.blnRestrictFolderOptAttr))

   88         parameters.Add(New SqlParameter("@blnForceSelectTemplate",objGroup.blnForceSelectTemplate))

   89         parameters.Add(New SqlParameter("@blnRestrictChangeLayout",objGroup.blnRestrictChangeLayout))

   90         parameters.Add(New SqlParameter("@blnRestrictAddElements",objGroup.blnRestrictAddElements))

   91         parameters.Add(New SqlParameter("@blnRestrictCreateTempFromArt",objGroup.blnRestrictCreateTempFromArt))

   92         parameters.Add(New SqlParameter("@blnAllowEventStatusUpdate",objGroup.blnAllowEventStatusUpdate))

   93         parameters.Add(New SqlParameter("@blnAllowVenueLimitUpdate",objGroup.blnAllowVenueLimitUpdate))

   94         parameters.Add(New SqlParameter("@blnAllowAddAttendee",objGroup.blnAllowAddAttendee))

   95         parameters.Add(New SqlParameter("@blnAllowDeleteAttendee",objGroup.blnAllowDeleteAttendee))

   96         parameters.Add(New SqlParameter("@blnAllowOnbeHalfRegistration",objGroup.blnAllowOnbeHalfRegistration))

   97         parameters.Add(New SqlParameter("@blnAllowMoveEvent",objGroup.blnAllowMoveEvent))


   99         ' partial event method to allow insertion of behaviour using partial class

  100         PreSave(parameters, objGroup, blnForUpdate)


  102         If blnForUpdate And  objGroup.intGroupID > 0   Then

  103             parameters.Add(New SqlParameter("@intGroupID",objGroup.intGroupID)) 

  104             sproc = "proc_GroupUpdate"

  105             ExecuteNonQuery(ConnectionString, CommandType.StoredProcedure, sproc, parameters)

  106         Else

  107             sproc = "proc_GroupInsert"


  109             Dim intNewKey as Integer

  110             intNewKey = CType(ExecuteScalar(ConnectionString, CommandType.StoredProcedure, sproc, parameters), Integer)   

  111             objGroup.intGroupID = intNewKey

  112         End If


  114         ' partial event method to allow insertion of behaviour using partial class

  115         PostSave(objGroup, blnForUpdate)


  117         ' refresh the cached version of this object

  118         Dim cacheName As StringString.Format("Group:{0}", ""  + objGroup.intGroupID.ToString() + "" )

  119         RemoveFromCache(cacheName)

  120         AddToCache(cacheName, objGroup)       

  121     End Sub


The end result for us was a much more flexible system of generated code which meant that we can now fix problems with out data classes in the templates directly and then just re-generate the code and replace the files in our project folders and commit to our source code repository (it helps that we use Subversion via TortoiseSVN).

Note: Thanks to Guy Burstein for taking the time to migrate the Copy Source as HTML addin to VS2008, even though it seems to have problems with my my colour scheme. Mazal Tov with that new MS job too!

No comments:

Post a Comment