How to Emulate Partial Classes in .NET 1.1 (LONG)

  • Thread starter Thread starter Mike Hofer
  • Start date Start date
M

Mike Hofer

I have a large .NET 1.1 application in which I have a discovered a need
to implement the functionality of partial classes. (To make a long
story short, a custom tool generates DAC classes, but I often want to
add functionality to those classes without losing those changes when
the classes are regenerated.)

So I find myself trying to figure out how to do this properly, and with
minimal impact on my existing code base. I think I have a plan, but I
would like your comments and suggestions for improvement or
alternatives.

Note that the tool in question was developed inhouse, so we can change
it as needed.

THE CURRENT SITUATION. The tool in question is responsible for
generating classes that exactly match the schema of the database. (The
tool is limited to SQL Server.)

For every table and view, a type safe class is created that contains
one property for every column. (For simplicity, I call this the model
class.) The model class's properties contain code to check for
ranges, null values, and so forth; also, a new instance of the class
initializes all properties to their default values as specified in the
database (that way, when you instantiate a new instance, it has all the
appropriate default values). Note that the model class is /not/ a
dataset. There should be /no/ direct connections between the model
class and any object in the System.Data.* namespaces; we want a full
layer of abstraction between the model and the database (that layer is
provided by the controller class).

Below, you can see an example of a simple table in our application, and
the class that the tool generates.

----------------------------------------------------------------------
Table:
+-----------------------------+
| <<Table>> |
| RaceList |
+-----------------------------+
| PK | ID | int (identity) |
| | Text | varchar(255) |
+-----------------------------+

Class:
Option Strict On

Public Class Race

Public Sub New()
End Sub

Private m_isDirty As Boolean = False
Public Property IsDirty() As Boolean
Get
Return m_isDirty
End Get
Set(ByVal value As Boolean)
m_isDirty = Value
End Set
End Property

Private m_ID As Integer = 0
Public Property ID() As Integer
Get
Return m_ID
End Get
Set(ByVal value As Integer)
If m_ID <> Value Then
m_ID = value
m_isDirty = True
End If
End Set
End Property

Private m_Text As String = String.Empty
Public Property Text() As String
Get
Return m_Text
End Get
Set(ByVal value As String)
If m_Text <> Value Then
If Not value Is Nothing AndAlso value.Length > 255 Then
Throw New ArgumentException("String too long")
End If
m_Text = value
m_isDirty = True
End If
End Set
End Property

End Class ' Race

(Note that the tool has built-in logic for identifying lookup tables,
so it mangled the name on this class, but you get the point.)

The tool also generates a type safe collection of model classes, and a
controller class that contains Exists, Delete, Load, and Save methods
for the model class. The controller class encapsulates all interaction
with the database, and works with instances of the model class. (The
controller class is permitted to reference the System.Data.*
namespaces.)

Finally, the tool generates a separate class for each stored procedure
in the database. For every parameter, there is a corresponding property
of the appropriate type. (Believe it or not, this was the original
reason the tool was created. For some reason, ADO.NET didn't include
type safe wrappers for stored procedures, and inlined all the SQL.)
This allows us to write code like this:

Dim proc As New InsertEmployeeProcedure

proc.FirstName = "John"
proc.LastName = "Doe"
proc.EmployeeNumber = 12795374

proc.ExecuteNonQuery()

THE PROBLEM. This scenario works, and usually works /very/ well.
However, there have been a few times when I wanted to add functionality
to the model or controller class, but couldn't because the tool would
have regenerated the classes and discarded my additions. So it occurs
to me that I need to be able to separate custom code from generated
code.

I considered using a region in the source code, but dropped the idea
because it would have added the inherent complexities of parsing the
source file and nested regions, and so forth and so on.

The perfect solution is partial classes. But alas, they aren't
available until 2.0, and the client isn't going to be an early
adopter. So I have to devise a solution that uses 1.1 technologies, and
do it in such a way that I don't break the code or make coding any
more complex than it already is.

THE PROPOSED SOLUTION. I want to emulate partial classes by putting all
the generated code in a base class, and creating "empty" derived
classes.

Model Classes: We will create two separate classes: the model base
class, and the actual model class. The model base class will look
exactly like the model class does now, except for the constructors. The
public constructor will be replaced by a protected friend constructor.
This will prevent the class from being inherited by anyone outside of
the current assembly. The new model class will inherit from the model
base class, and will initially include no additional functionality.
However, when you need to add functionality to the model, you add it to
THIS class.

Collection Classes: The collection classes will remain type safe, but
they will take members of the new model class, and not of the base
class.

Controller Classes: The tool will create two separate classes: the
controller base class, and the actual controller class. The controller
base class will look exactly as it does now, but will contain only one
constructor (with protected friend access).

The resulting classes would look like this:

+------------------------------+ +-------+
+ RaceModel | + Race |
+------------------------------+ <-- +-------+
+------------------------------+ +-------+
| ~ New | + + New |
| + <<property>> ID : Integer | +-------+
| + <<property>> Text : String |
+------------------------------+

+----------------------------+
+ RaceCollection |
+----------------------------+
+ + Count : Integer +
+ + Item(Index) : Race |
+----------------------------+
+ + New() |
+ + Add(Race) : Integer |
+ + Contains(Race) : Boolean |
+ + IndexOf(Race) : Integer |
+ + Insert(Integer, Race) |
+ + Remove(Race) |
+----------------------------+

+--------------------------------+ +----------------+
| RaceControllerBase | | RaceController |
+--------------------------------+ +----------------+
+--------------------------------+ +----------------+
| ~ New | <-- | + New() |
| + Exists(Integer) : Boolean | | + Delete(Race) |
| ~ Delete(Race, SqlTransaction) | | + Save(Race) |
| ~ Insert(Race, SqlTransaction) | +----------------+
| + Load(Integer) : Race |
| ~ Update(Race) |
| ~ Update(Race, SqlTransaction) |
+--------------------------------+

(I used the tilde [~] to indicate friend methods.) Each class would
reside in a separate file. Any custom functionality would be placed on
the Race or RaceController class; the RaceModel and RaceControllerBase
classes will be overwritten by the tool as requested by the user.

When the tool generates classes, it will always generate the base
classes, but will generate the derived classes only if they do not
already exist. In that way, you can have the tool generate your base
classes, where the classes /must/ always match the database schema, and
retain any additional code you've added to the derived model and
controller. This allows the tool to keep the classes in synch with the
schema, while retaining your custom code.

SUMMARY. I need a solution that will help me to separate my generated
database code from my custom database code. The solution has to be
relatively simple to understand, and must not add an inordinate amount
of complexity to the system. Finally, the solution must have minimal
impact on the body of the code that currently uses these components. I
/think/ this solution solves that problem, but I'm sure it could use
improvement.

I look forward to your comments and suggestions.
 
Why you can't just derive new class from your generated one and add
functionality there?

New class will reside in separate file. If you regenerate underlying class,
you need only to recompile.
Did I miss something?


Mike Hofer said:
I have a large .NET 1.1 application in which I have a discovered a need
to implement the functionality of partial classes. (To make a long
story short, a custom tool generates DAC classes, but I often want to
add functionality to those classes without losing those changes when
the classes are regenerated.)

So I find myself trying to figure out how to do this properly, and with
minimal impact on my existing code base. I think I have a plan, but I
would like your comments and suggestions for improvement or
alternatives.

Note that the tool in question was developed inhouse, so we can change
it as needed.

THE CURRENT SITUATION. The tool in question is responsible for
generating classes that exactly match the schema of the database. (The
tool is limited to SQL Server.)

For every table and view, a type safe class is created that contains
one property for every column. (For simplicity, I call this the model
class.) The model class's properties contain code to check for
ranges, null values, and so forth; also, a new instance of the class
initializes all properties to their default values as specified in the
database (that way, when you instantiate a new instance, it has all the
appropriate default values). Note that the model class is /not/ a
dataset. There should be /no/ direct connections between the model
class and any object in the System.Data.* namespaces; we want a full
layer of abstraction between the model and the database (that layer is
provided by the controller class).

Below, you can see an example of a simple table in our application, and
the class that the tool generates.

----------------------------------------------------------------------
Table:
+-----------------------------+
| <<Table>> |
| RaceList |
+-----------------------------+
| PK | ID | int (identity) |
| | Text | varchar(255) |
+-----------------------------+

Class:
Option Strict On

Public Class Race

Public Sub New()
End Sub

Private m_isDirty As Boolean = False
Public Property IsDirty() As Boolean
Get
Return m_isDirty
End Get
Set(ByVal value As Boolean)
m_isDirty = Value
End Set
End Property

Private m_ID As Integer = 0
Public Property ID() As Integer
Get
Return m_ID
End Get
Set(ByVal value As Integer)
If m_ID <> Value Then
m_ID = value
m_isDirty = True
End If
End Set
End Property

Private m_Text As String = String.Empty
Public Property Text() As String
Get
Return m_Text
End Get
Set(ByVal value As String)
If m_Text <> Value Then
If Not value Is Nothing AndAlso value.Length > 255 Then
Throw New ArgumentException("String too long")
End If
m_Text = value
m_isDirty = True
End If
End Set
End Property

End Class ' Race

(Note that the tool has built-in logic for identifying lookup tables,
so it mangled the name on this class, but you get the point.)

The tool also generates a type safe collection of model classes, and a
controller class that contains Exists, Delete, Load, and Save methods
for the model class. The controller class encapsulates all interaction
with the database, and works with instances of the model class. (The
controller class is permitted to reference the System.Data.*
namespaces.)

Finally, the tool generates a separate class for each stored procedure
in the database. For every parameter, there is a corresponding property
of the appropriate type. (Believe it or not, this was the original
reason the tool was created. For some reason, ADO.NET didn't include
type safe wrappers for stored procedures, and inlined all the SQL.)
This allows us to write code like this:

Dim proc As New InsertEmployeeProcedure

proc.FirstName = "John"
proc.LastName = "Doe"
proc.EmployeeNumber = 12795374

proc.ExecuteNonQuery()

THE PROBLEM. This scenario works, and usually works /very/ well.
However, there have been a few times when I wanted to add functionality
to the model or controller class, but couldn't because the tool would
have regenerated the classes and discarded my additions. So it occurs
to me that I need to be able to separate custom code from generated
code.

I considered using a region in the source code, but dropped the idea
because it would have added the inherent complexities of parsing the
source file and nested regions, and so forth and so on.

The perfect solution is partial classes. But alas, they aren't
available until 2.0, and the client isn't going to be an early
adopter. So I have to devise a solution that uses 1.1 technologies, and
do it in such a way that I don't break the code or make coding any
more complex than it already is.

THE PROPOSED SOLUTION. I want to emulate partial classes by putting all
the generated code in a base class, and creating "empty" derived
classes.

Model Classes: We will create two separate classes: the model base
class, and the actual model class. The model base class will look
exactly like the model class does now, except for the constructors. The
public constructor will be replaced by a protected friend constructor.
This will prevent the class from being inherited by anyone outside of
the current assembly. The new model class will inherit from the model
base class, and will initially include no additional functionality.
However, when you need to add functionality to the model, you add it to
THIS class.

Collection Classes: The collection classes will remain type safe, but
they will take members of the new model class, and not of the base
class.

Controller Classes: The tool will create two separate classes: the
controller base class, and the actual controller class. The controller
base class will look exactly as it does now, but will contain only one
constructor (with protected friend access).

The resulting classes would look like this:

+------------------------------+ +-------+
+ RaceModel | + Race |
+------------------------------+ <-- +-------+
+------------------------------+ +-------+
| ~ New | + + New |
| + <<property>> ID : Integer | +-------+
| + <<property>> Text : String |
+------------------------------+

+----------------------------+
+ RaceCollection |
+----------------------------+
+ + Count : Integer +
+ + Item(Index) : Race |
+----------------------------+
+ + New() |
+ + Add(Race) : Integer |
+ + Contains(Race) : Boolean |
+ + IndexOf(Race) : Integer |
+ + Insert(Integer, Race) |
+ + Remove(Race) |
+----------------------------+

+--------------------------------+ +----------------+
| RaceControllerBase | | RaceController |
+--------------------------------+ +----------------+
+--------------------------------+ +----------------+
| ~ New | <-- | + New() |
| + Exists(Integer) : Boolean | | + Delete(Race) |
| ~ Delete(Race, SqlTransaction) | | + Save(Race) |
| ~ Insert(Race, SqlTransaction) | +----------------+
| + Load(Integer) : Race |
| ~ Update(Race) |
| ~ Update(Race, SqlTransaction) |
+--------------------------------+

(I used the tilde [~] to indicate friend methods.) Each class would
reside in a separate file. Any custom functionality would be placed on
the Race or RaceController class; the RaceModel and RaceControllerBase
classes will be overwritten by the tool as requested by the user.

When the tool generates classes, it will always generate the base
classes, but will generate the derived classes only if they do not
already exist. In that way, you can have the tool generate your base
classes, where the classes /must/ always match the database schema, and
retain any additional code you've added to the derived model and
controller. This allows the tool to keep the classes in synch with the
schema, while retaining your custom code.

SUMMARY. I need a solution that will help me to separate my generated
database code from my custom database code. The solution has to be
relatively simple to understand, and must not add an inordinate amount
of complexity to the system. Finally, the solution must have minimal
impact on the body of the code that currently uses these components. I
/think/ this solution solves that problem, but I'm sure it could use
improvement.

I look forward to your comments and suggestions.
 
AlexS said:
Why you can't just derive new class from your generated one and add
functionality there?

New class will reside in separate file. If you regenerate underlying class,
you need only to recompile.
Did I miss something?

That is, actually, what I'm proposing. (With the single caveat, of
course, that I can't disrupt the class names that were previously
generated without affecting the existing code base.) The generated code
is a base class; I would provide a number of default classes that are
derived from it, but would never regenerate the derived classes again.

So yeah, we're violently agreeing. :)

What I was interested in was any way that anyone could see a problem
with the methodology. Perhaps there's some reason it won't work;
something I'm overlooking.
Mike Hofer said:
I have a large .NET 1.1 application in which I have a discovered a need
to implement the functionality of partial classes. (To make a long
story short, a custom tool generates DAC classes, but I often want to
add functionality to those classes without losing those changes when
the classes are regenerated.)

So I find myself trying to figure out how to do this properly, and with
minimal impact on my existing code base. I think I have a plan, but I
would like your comments and suggestions for improvement or
alternatives.

Note that the tool in question was developed inhouse, so we can change
it as needed.

THE CURRENT SITUATION. The tool in question is responsible for
generating classes that exactly match the schema of the database. (The
tool is limited to SQL Server.)

For every table and view, a type safe class is created that contains
one property for every column. (For simplicity, I call this the model
class.) The model class's properties contain code to check for
ranges, null values, and so forth; also, a new instance of the class
initializes all properties to their default values as specified in the
database (that way, when you instantiate a new instance, it has all the
appropriate default values). Note that the model class is /not/ a
dataset. There should be /no/ direct connections between the model
class and any object in the System.Data.* namespaces; we want a full
layer of abstraction between the model and the database (that layer is
provided by the controller class).

Below, you can see an example of a simple table in our application, and
the class that the tool generates.

----------------------------------------------------------------------
Table:
+-----------------------------+
| <<Table>> |
| RaceList |
+-----------------------------+
| PK | ID | int (identity) |
| | Text | varchar(255) |
+-----------------------------+

Class:
Option Strict On

Public Class Race

Public Sub New()
End Sub

Private m_isDirty As Boolean = False
Public Property IsDirty() As Boolean
Get
Return m_isDirty
End Get
Set(ByVal value As Boolean)
m_isDirty = Value
End Set
End Property

Private m_ID As Integer = 0
Public Property ID() As Integer
Get
Return m_ID
End Get
Set(ByVal value As Integer)
If m_ID <> Value Then
m_ID = value
m_isDirty = True
End If
End Set
End Property

Private m_Text As String = String.Empty
Public Property Text() As String
Get
Return m_Text
End Get
Set(ByVal value As String)
If m_Text <> Value Then
If Not value Is Nothing AndAlso value.Length > 255 Then
Throw New ArgumentException("String too long")
End If
m_Text = value
m_isDirty = True
End If
End Set
End Property

End Class ' Race

(Note that the tool has built-in logic for identifying lookup tables,
so it mangled the name on this class, but you get the point.)

The tool also generates a type safe collection of model classes, and a
controller class that contains Exists, Delete, Load, and Save methods
for the model class. The controller class encapsulates all interaction
with the database, and works with instances of the model class. (The
controller class is permitted to reference the System.Data.*
namespaces.)

Finally, the tool generates a separate class for each stored procedure
in the database. For every parameter, there is a corresponding property
of the appropriate type. (Believe it or not, this was the original
reason the tool was created. For some reason, ADO.NET didn't include
type safe wrappers for stored procedures, and inlined all the SQL.)
This allows us to write code like this:

Dim proc As New InsertEmployeeProcedure

proc.FirstName = "John"
proc.LastName = "Doe"
proc.EmployeeNumber = 12795374

proc.ExecuteNonQuery()

THE PROBLEM. This scenario works, and usually works /very/ well.
However, there have been a few times when I wanted to add functionality
to the model or controller class, but couldn't because the tool would
have regenerated the classes and discarded my additions. So it occurs
to me that I need to be able to separate custom code from generated
code.

I considered using a region in the source code, but dropped the idea
because it would have added the inherent complexities of parsing the
source file and nested regions, and so forth and so on.

The perfect solution is partial classes. But alas, they aren't
available until 2.0, and the client isn't going to be an early
adopter. So I have to devise a solution that uses 1.1 technologies, and
do it in such a way that I don't break the code or make coding any
more complex than it already is.

THE PROPOSED SOLUTION. I want to emulate partial classes by putting all
the generated code in a base class, and creating "empty" derived
classes.

Model Classes: We will create two separate classes: the model base
class, and the actual model class. The model base class will look
exactly like the model class does now, except for the constructors. The
public constructor will be replaced by a protected friend constructor.
This will prevent the class from being inherited by anyone outside of
the current assembly. The new model class will inherit from the model
base class, and will initially include no additional functionality.
However, when you need to add functionality to the model, you add it to
THIS class.

Collection Classes: The collection classes will remain type safe, but
they will take members of the new model class, and not of the base
class.

Controller Classes: The tool will create two separate classes: the
controller base class, and the actual controller class. The controller
base class will look exactly as it does now, but will contain only one
constructor (with protected friend access).

The resulting classes would look like this:

+------------------------------+ +-------+
+ RaceModel | + Race |
+------------------------------+ <-- +-------+
+------------------------------+ +-------+
| ~ New | + + New |
| + <<property>> ID : Integer | +-------+
| + <<property>> Text : String |
+------------------------------+

+----------------------------+
+ RaceCollection |
+----------------------------+
+ + Count : Integer +
+ + Item(Index) : Race |
+----------------------------+
+ + New() |
+ + Add(Race) : Integer |
+ + Contains(Race) : Boolean |
+ + IndexOf(Race) : Integer |
+ + Insert(Integer, Race) |
+ + Remove(Race) |
+----------------------------+

+--------------------------------+ +----------------+
| RaceControllerBase | | RaceController |
+--------------------------------+ +----------------+
+--------------------------------+ +----------------+
| ~ New | <-- | + New() |
| + Exists(Integer) : Boolean | | + Delete(Race) |
| ~ Delete(Race, SqlTransaction) | | + Save(Race) |
| ~ Insert(Race, SqlTransaction) | +----------------+
| + Load(Integer) : Race |
| ~ Update(Race) |
| ~ Update(Race, SqlTransaction) |
+--------------------------------+

(I used the tilde [~] to indicate friend methods.) Each class would
reside in a separate file. Any custom functionality would be placed on
the Race or RaceController class; the RaceModel and RaceControllerBase
classes will be overwritten by the tool as requested by the user.

When the tool generates classes, it will always generate the base
classes, but will generate the derived classes only if they do not
already exist. In that way, you can have the tool generate your base
classes, where the classes /must/ always match the database schema, and
retain any additional code you've added to the derived model and
controller. This allows the tool to keep the classes in synch with the
schema, while retaining your custom code.

SUMMARY. I need a solution that will help me to separate my generated
database code from my custom database code. The solution has to be
relatively simple to understand, and must not add an inordinate amount
of complexity to the system. Finally, the solution must have minimal
impact on the body of the code that currently uses these components. I
/think/ this solution solves that problem, but I'm sure it could use
improvement.

I look forward to your comments and suggestions.
 
Hey Mike,
What I was interested in was any way that anyone could see a problem
with the methodology. Perhaps there's some reason it won't work;
something I'm overlooking.

Until now I thought partial classes are a really cool feature but then I
started thinking what actually the difference between two partial classes
and a base and an inherited class are. Here is my actually pretty poor
result:

-Partial classes allow to spread code across files that would not compile if
you only took one or the other file. With derived classes, at least the base
class will compile w/o the derived class (great for code generators). For
example a set of variables is defined in one file (to be changed by the
developer) and the code referencing these variables in the other file. Since
you have control over your code generator that issue doesn't matter.

-I think with inheritance there comes a small performance penalty when
invoking members because the call has to be made to the correct interface.
However, in most applications this will be neglectable.

-In cases where reflection comes into play this might play a role but so far
I couldn't come up with a relevant example.

That's about it what I could think of at 11:30pm ;-) Let me know if you come
up with other differences that might matter.

Christoph
 
Hey Mike,
We here also use a business class generator that was built in-house.
What we did to resolve this issue was to modify the generator to be
aware of class templates.

For example...
We want to customize the UserInfo class to include a method that checks
for a certain User Type...

File Name: UserInfo.vb.template
Code...

' The Region is not necessary...
#Region " Customized Method(s) "
Public Function IsAdministrator() As Boolean
...
End Function
#End Region

When the generator is used and the application gets to the UserInfo
table, it will see that there is a template and append the template
code to the generated code. We can customize any generated class we
want just by creating a file that has the same file name as the class
but with the ".vb.template" extension.

Just another idea for you to dwell on.
 
Ur said:
Hey Mike,
We here also use a business class generator that was built in-house.
What we did to resolve this issue was to modify the generator to be
aware of class templates.

For example...
We want to customize the UserInfo class to include a method that checks
for a certain User Type...

File Name: UserInfo.vb.template
Code...

' The Region is not necessary...
#Region " Customized Method(s) "
Public Function IsAdministrator() As Boolean
...
End Function
#End Region

When the generator is used and the application gets to the UserInfo
table, it will see that there is a template and append the template
code to the generated code. We can customize any generated class we
want just by creating a file that has the same file name as the class
but with the ".vb.template" extension.

Just another idea for you to dwell on.

So instead of inheriting, you're having the user create the template
separately, then you regenerate the classes. During regeneration, the
dynamic portion is spliced together with the template portion to
produce the actual file.

I can see a couple of problems with that:

1.) If you change the template, you have to regnerate the class, or it
won't include the modification (unless you do the copy-paste thing,
which is icky).

2.) The file extension could be a problem for the IDE. If it's not .VB,
it'll probably edit it as a text file, so you won't get the benefits of
code completion, syntax highlighting, and so forth. That could mean
many different attempts to regenerate the code before you get it "just
right." You could mitigate that somewhat by switching the extension
around, but then you'd still have to create a project with lots of
extra files in it.

How did you get around these issues? Or am I not understanding the
picture?

Thanks for the input!
 
:

However, there have been a few times when I wanted to add
functionality
to the model or controller class, but couldn't because the tool would
have regenerated the classes and discarded my additions. So it occurs
to me that I need to be able to separate custom code from generated
code.

I considered using a region in the source code, but dropped the idea
because it would have added the inherent complexities of parsing the
source file and nested regions, and so forth and so on.

The perfect solution is partial classes. But alas, they aren't
available until 2.0, and the client isn't going to be an early
adopter. So I have to devise a solution that uses 1.1 technologies,
and
do it in such a way that I don't break the code or make coding any
more complex than it already is.

THE PROPOSED SOLUTION. I want to emulate partial classes by putting
all
the generated code in a base class, and creating "empty" derived
classes.

This is the approach I took with my state machine toolkit using CodeDom:

http://www.codeproject.com/csharp/StateMachineToolkitPrtIII.asp

You can make the base class abstract. Also, you can put abstract members
in your base class that must be overriden in your derived class. This
ensures that certain functionality gets implemented, which can help if
your derived class is written by hand. I've found that sometimes
regenerating the base class does require some changes in the derived
class, but these can be small depending on the change that has been
made. The important thing is that the functionality in the derived class
is never overwritten.
 
Mike said:
I have a large .NET 1.1 application in which I have a discovered a
need to implement the functionality of partial classes. (To make a
long story short, a custom tool generates DAC classes, but I often
want to add functionality to those classes without losing those
changes when the classes are regenerated.)
(snip)

SUMMARY. I need a solution that will help me to separate my generated
database code from my custom database code. The solution has to be
relatively simple to understand, and must not add an inordinate amount
of complexity to the system. Finally, the solution must have minimal
impact on the body of the code that currently uses these components. I
/think/ this solution solves that problem, but I'm sure it could use
improvement.

In LLBLGen Pro, which has a hierarchical task based code generator
engine to generate code, I solved it in the following way. First some
requirements:

- there should be regions in the generated code which are 'safe havens'
for user added code, I call then user code regions.
- every user code region should be scope aware. This means that it
should be possible to add code to a generated class header, generated
method structure etc. This is the hard part.

Especially point two is hard to do. Most code generators out there
which support 'merging' simply merge a code file with custom code at a
given spot at the end of the file. But what if you want to do this:

public class Foo
// add interfaces here
{
// ...

public Foo()
{
InitClass();
}

//...


private void InitClass()
{
// init code

// here you want to add extra code
}
}

? how do you merge that code inside generated code and at generate
time?

solution: In your template engine, define a statement which marks a
user code region start and a statement which defines an end. Also
define start, end markers with comments, like I use:
// LLBLGEN_PRO_USER_CODE_REGION_START name
// custom code goes here.
// LLBLGEN_PRO_USER_CODE_REGION_END

Now, in my template I define user code regions for example in the
InitClass method in the example above, or right after the class header.
These regions have a name, which is unique per file.

When the engine has to generate a file, it first checks if the file
already exists. If so, it loads the file, parses it for user code
regions (using the comment markers) and stores the contents per region
under a name.

Then, each time a user code region definition is seen in the template,
the read regions from the file are checked, and if present (using the
same name) the contents of the region read from the existing file is
stored at the place where the region was defined in the template.

You can also generate the names for regions using epxressions if you
like, for example if you generate code in a loop and you want to define
in each loop iteration a region.

A simpler way is to use include templates. Define a statement which
defines the include of another template and define the code there. It
of course depends on the system you're using how flexible this is: if
you're using template ID's bound to template files (like I do) you can
create very generic templates with include statements using template
ID's and depending on the definition used at runtime (of the generator)
which file is bound to which templateid, different code is generated.

Yes it takes a lot of work, but flexibility doesn't come easy ;)

Frans

--
 
Yes... one of the issues with the template schema is that you would
have to regen the class. We got around this by modifying the generator
to allow us to select which class (or table) we want to regenerate vs
having to regen ALL the classes (which is a major pain in the neck).

Regarding the second issue... The way we dealt with this problem was to
develop the class "additions" (i.e. custom properties, etc.) in the
actual class itself. We then have the intellisense and all the bells
and whistles of the IDE. After we know that the custom addition is
stable with the desired effects, we either create a template or append
it to the existing template. Regardless, the code has to be written.
The template just ensures that the custom code stays with the class
if/when the class gets regenerated. This also helps with the first
issue.
 
Back
Top