| |
DevForce® Classic Tech Tips |
|
|
|
| Create a Generic When T is unknown |  |
| Create a Generic
When T is unknown |
Level 400
DevForce Express |
Sep 19, 06 |
I like to write generic code
that I can reuse to solve similar
problems throughout my application.
So it is something of a shock to
discover that .NET generics can
interfere with my ability to write
generic code. Maybe they should
have been called “specifics”.
Imagine I’ve got an idea
for a general routine that works
on a concrete type based on the
generic collection, BindableList(Of
T). I won’t know
the type, ‘T’,
until runtime.
There could be many reasons
why I won’t know the type
of ‘T’.
Perhaps I’ll present the
user with a ComboBox of
business objects types. Once
the user picks a type, I fetch
every instance of this type from
the database and pass the result
on to my routine which pours
those entities into a BindableList of
that type, sorts it, and shoots
it off to my UI screen builder.
The screen builder reads the
properties of the items in the
list, lays down some UI controls,
and binds those controls to the
item properties. Pretty cool.
My routine will instantiate
a BindableList(Of SelectedType).
Of course I can’t write
this directly into my code because SelectedType could
be an Employee,
or a Company,
or who knows what.
I’m clever enough to
have ensured that all my business
objects inherit from a base Entity class.
So I make my routine instantiate
a BindableList(Of
Entity), fill it with
the business objects, sort it
and return a result of BindableList(Of
Entity). I can do that,right?
No, I can’t.
BindableList(Of
Entity) is not the same
type as BindableList(Of
Employee), or BindableList(Of
Customer). It matters
not a jot that Employee inherits
from Entity.
My compile fails if I insert
an Employee into
a BindableList(Of
Entity). The application
will throw a runtime exception
if I try to fake it out by
casting through interfaces.
While I can assign an Employee
instance to a variable declared
as Entity,
I cannot assign an object of
type Generic(Of
Employee) to an object
of type Generic(Of
Entity). Although Employee
inherits from Entity,
the Generic(Of
Employee) class does not
inherit from Generic(Of
Entity).
I’m stuck.
Fortunately, .NET reflection
can bail me out. I can use reflection
to create a BindableList(Of
T) at runtime when I know
what ‘T’ is.
Because BindableList(Of
T) inherits from non-generic
interfaces, I can proceed with
my databinding example by exploiting
one of those interfaces. If there
is something I need that isn’t
accessible through an interface,
a sorting method perhaps, I can
reflect again to invoke that
method.
The following example guides
you through the brambles and
puts you on the clear path to
reusable generic code.
Code Example
|
C#:
public IBindingList CreateBindableListAtRuntime() {
// Get some entities of the runtime type (e.g., Employee)
Type entityType = typeof(Employee);
IEnumerable entities =
aPersistenceManager.GetEntities(entityType);
// Set sort parameters
string sortProperty = "FullName";
ListSortDirection sortDirection = ListSortDirection.Ascending;
return CreateAndSort(
entityType, entities, sortProperty, sortDirection);
}
private IBindingList CreateAndSort(
Type pTargetType, IEnumerable pObjects,
string pSortPropertyName, ListSortDirection pSortDirection) {
// (1) Create a generic BindableList of the desired type
IBindingList result = (IBindingList)
IdeaBlade.Util.TypeFns.ConstructGenericInstance(
typeof(BindableList<>), new Type[] { pTargetType });
// (2) Add objects to the BindableList (not type checked!)
foreach ( object item in pObjects ) { result.Add(item); }
// (3) Sort it using reflection
SortBindableList(
result, pSortPropertyName, pSortDirection, false);
return result;
}
// (4) Sort BindableList using reflection
private static void SortBindableList(
IBindingList pList, params object[] pParms) {
// (5) Sort method name
string method="ApplySort";
// (6) Flags to invoke a public instance method
BindingFlags flags =
BindingFlags.InvokeMethod |
BindingFlags.Public | BindingFlags.Instance;
// (7) Get the concrete type of our list and
// invoke the method via reflection
Type listType = pList.GetType();
listType.InvokeMember(method, flags, null, pList, pParms);
}
VB.NET:
Public Function CreateBindableListAtRuntime() As IBindingList
' Get some entities of the runtime type (e.g., Employee)
Dim entityType As Type = GetType(Employee)
Dim entities As IEnumerable
entities = aPersistenceManager.GetEntities(entityType)
' Set sort parameters
Dim sortProperty As String = "FullName"
Dim sortDirection As ListSortDirection
sortDirection = ListSortDirection.Ascending
Return CreateAndSort( _
entityType, entities, sortProperty, sortDirection)
End Function
Private Function CreateAndSort( _
ByVal pTargetType As Type, _
ByVal pObjects As IEnumerable, _
ByVal pSortPropertyName As String, _
ByVal pSortDirection As ListSortDirection) As IBindingList
' (1) Create a generic BindableList of the desired type
Dim temp as Object
temp = IdeaBlade.Util.TypeFns.ConstructGenericInstance( _
GetType(BindableList(Of ) ), _
New Type() { pTargetType } )
Dim result As IBindingList
result = CType(temp, IBindingList)
' (2) Add objects to the BindableList (not type checked!)
For Each item As Object In pObjects
result.Add(item)
Next item
' (3) Sort it using reflection
SortBindableList( _
result, pSortPropertyName, pSortDirection, False)
Return result
End Function
' (4) Sort BindableList using reflection
Private Shared Sub SortBindableList( _
ByVal pList As IBindingList, _
ParamArray ByVal pParms As Object())
' (5) Sort method name
Dim method As String="ApplySort"
' (6) Flags to invoke a public instance method
Dim flags As BindingFlags
flags = BindingFlags.InvokeMethod Or _
BindingFlags.Public Or _
BindingFlags.Instance
' (7) Get the concrete type of our list and
' invoke the method via reflection
Dim listType As Type = CType(pList, Object).GetType()
listType.InvokeMember(method, flags, Nothing, pList, pParms)
End Sub
Code Commentary |
| CreateBindableListAtRuntime() is
simply a harness for the real
action in CreateAndSort and
its supporting methods. |
1)
|
Statement #1
of CreateAndSort creates
an instance of the BindableList(Of
T) for an arbitrary
type ‘T’ which
we pass in as a parameter.
We’re relying on a DevForce
utility method called ConstructGenericInstance;
see the “End Note” for
more on this. |
| |
Observe that
we have to hold that instance
in something known and declarable
at compile time. We do that
in “result” which
is declared to be an IBindingList,
an interface implemented
by BindingList(Of
T) which is the .NET
base class for BindableList(Of
T).
We could have chosen from
a number of interfaces
implemented by BindingList.
We picked the interface
which has the richest API,
the most methods and properties.
|
| |
|
As
a general rule, in the absence
of any other compelling interest,
it is best to return as rich
and specific an object as possible,
in hopes that the caller won’t
have to cast the returned result. |
|
| 2) |
Statement
#2 pours the contents of
an IEnumerable parameter
into our result. In our example,
the IEnumerable is
an Entity array
returned by a DevForce PersistenceManager.GetEntities method.
CreateAndSort doesn’t
know that. It just knows
it will get some kind of IEnumerable.
|
|
|
|
In general,
we want our method parameters
to accept the broadest possible
type as long as that type
is consistent with the method’s
purpose.
Of course Object is
the broadest possible type.
It’s too broad. We
know we have to enumerate
over the objects to get
them into the BindableList.
Why make ourselves verify
that it is enumerable at
runtime when the compiler
can do it.
|
|
| |
CreateAndSort should
confirm that all of the objects
are of the same type, the
specified type. It doesn’t,
which means we could get
a runtime exception.
We can’t delegate
this job to the compiler.
We don’t know the
type until runtime, so
we can’t specify
what type of items we’ll
enumerate. Nor can we use
generics to enforce that
all items are the same
type; remember, we’re
doing this exercise because
we’re struggling
to create just such a generic
at runtime.
CreateAndSort is
a private method, so the
risk is low. Still, such
methods have a way of becoming
public. We’ll leave
it to you to add the type
checking.
|
| 3) |
Now we want
to do something with that
generic BindableList.
We’re going to sort
it.
The easiest and most
direct way would be to
call the ApplySort method
of IBindingList which,
not coincidentally, is
the declared type of our
generic result variable.
Unfortunately, ApplySort takes
a PropertyDescriptor for
the property to sort on,
and we only know the name
of that property.
DevForce has some nifty
ways to construct the PropertyDescriptor from
the name, but that’s
out of scope for this lesson.
Instead, we’ll
use reflection to take
advantage of our certainty
that the result is actually
a BindableList(Of
T). A BindableList(Of
T) has an ApplySort override
that takes a property name.
|
| 4) |
Check out
the parameters of the SortBindableList method.
The first holds our newly
minted BindableList.
Then comes pParms,
an array of object,
in which to pass the parameters
of the ApplySort method.
Note the params keyword
(ParamArray in
VB). This
is a sweet bit of syntactic
sugar. Without it, the
caller would have to construct
an Object array
and populate it with the
parameters to ApplySort.
Instead she can simply
string them together at
the end of the call, as
in “… pSortPropertyName, pSortDirection,
false)”.
These are the three parameters
for the intended overload
of ApplySort: the
name of the property to
sort on, the sort direction,
and a flag that means “don’t
bother to keep the list
sorted.”
|
| 5) |
Here, we’ve
inscribed the name of the BindableList method
we want to call. |
| 6) |
We build up “binding” flags
to tell Reflection what
we want to do. In this case,
we want it to invoke a public
instance method. |
| 7) |
We get the
concrete type of the list.
No, we can’t use the
type of IBindableList.
We wouldn’t bother
with all of this reflection
business if we could; we’d
just call the method through
the interface. But there
is no suitable method on IBindableList,
so we have to call a method
on the concrete class which
is some flavor of BindableList(Of
T).
Finally, we tell that
concrete type to reflectively
invoke our method. We pass
in the method name, the
binding flags, a null for
the Binder (we’ll
let .NET use the default),
the list to sort, and the
parameters of our sort
method.
|
Refactoring
Opportunity
SortBindableList has
the potential to be a fully
general approach to invoking
any method of any type.
Right now it requires an IBindingList,
and we’ve baked a
specific method name and
binding flags right into
the code.
Look at it harder and
see if you can remove these
dependencies. We did so
in the latest version of
Funhouse where the method, ExtendedReflectionFns.InvokeMethod,
in the IdeaBlade.Extension project
would be easy to substitute
for SortBindableList in
this example.
End Note
The ConstructGenericInstance plays
an important role in the
code sample. It’s
in the DevForce utility
library, IdeaBlade.Util.
You’ve got DevForce,
right? You don’t?
Well, here’s the
source for this method:
|
|
C#:
/// <summary>Make an instance of a generic type</summary>
/// <param name="pGenericType">
/// Type of the generic, e.g. typeof(EntityList<>)
/// </param>
/// <param name="pTypeParms">
/// TypesParameters, e.g., typeOf(Employee)
/// </param>
/// <param name="pConstructorParams">
/// Parameters to pass to the constructor
/// </param>
/// <returns>A new instance of the generic type</returns>
public static Object ConstructGenericInstance(
Type pGenericType, Type[] pTypeParms,
params Object[] pConstructorParams) {
Type finalType = pGenericType.MakeGenericType(pTypeParms);
return Activator.CreateInstance(finalType, pConstructorParams);
}
VB.NET:
''' <summary>Make an instance of a generic type</summary>
''' <param name="pGenericType">
''' Type of the generic, e.g., GetType(EntityList())
''' </param>
''' <param name="pTypeParms">
''' TypesParameters, e.g., GetType(Employee)
''' </param>
''' <param name="pConstructorParams">
''' Parameters to pass to the constructor
''' </param>
''' <returns>A new instance of the generic type</returns>
Public Shared Function ConstructGenericInstance( _
ByVal pGenericType As Type, _
ByVal pTypeParms As Type(), _
ParamArray ByVal pConstructorParams As Object()) _
As Object
Dim finalType As Type
finalType = pGenericType.MakeGenericType(pTypeParms)
Return Activator.CreateInstance(finalType, pConstructorParams)
End Function
|
|
|
|
|
|