Background
Occasionally we find ourselves in a situation where we have an object, and we want to determine which of several types of object it is in order to do something with it. For instance, we may have a Control, and if it's a Button we may want to do one thing with it, but if it's a TextBox we may want to do something else with it.
A fast, simple, and reliable way of handling that is:
void F ( object item )
{
if ( item is Button ) ...
else if ( item is TextBox ) ...
}
Of great importance here is that this technique handles derived types properly. (It also works with interfaces, the other techniques presented here don't.)
This works very well, but can become unwieldy as more and more tests are added, both in terms of maintenance and performance.
Obviously, a switch
would help smooth things out, but we can't use the type directly. The following won't compile:
void F ( object item )
{
switch ( item.GetType() )
{
case Button : ...
case TextBox : ...
}
}
We can use strings with switch
statements, so two common techniques are:
void F ( object item )
{
switch ( item.GetType().Name )
{
case "Button" : ...
case "TextBox" : ...
}
}
and
void F ( object item )
{
switch ( item.GetType().FullName )
{
case "System.Windows.Forms.Button" : ...
case "System.Windows.Forms.TextBox" : ...
}
}
There are also some among us who argue against using strings in switch statements; for them I submit the following:
enum Controls
{
Button
,
TextBox
...
}
void F ( object item )
{
if ( Enum.IsDefined ( typeof(Controls) , item.GetType().Name ) )
{
switch ( (Controls) Enum.Parse ( typeof(Controls) , item.GetType().Name ) )
{
case Controls.Button : ...
case Controls.TextBox : ...
}
}
}
None of these techniques will handle derived types properly:
When passed an item of a type that derives from Button, but isn't named "Button" it won't pass the test though it probably should (false-negative).
In the first and third snippets, when passed an item of a type that doesn't derive from Button, but is named "Button" it will pass the test and probably cause an InvalidCastException (false-positive).
Therefore, while these techniques may be "good enough" in many cases, they are not completely reliable, so I feel that they are not suitable as general solutions.
Using an EnumTransmogrifier
The idea of using of an enumeration and Enum.Parse
is actually pretty good, but it's subject to false-positives because we're limited to using the Name of the type rather than the Fullname. (Because the FullName contains characters that aren't allowed in a simple name.)
Mapping strings to enumeration values is what my EnumTransmogrifier [^] is all about:
enum Controls
{
[Description("System.Windows.Forms.Button")]
Button
,
[Description("System.Windows.Forms.TextBox")]
TextBox
...
}
EnumTransmogrifier<Controls> controls = new EnumTransmogrifier<Controls>() ;
void F ( object item )
{
Controls control ;
if ( controls.TryParse ( item.GetType().FullName , out control ) )
{
switch ( control )
{
case Controls.Button : ...
case Controls.TextBox : ...
}
}
}
That takes care of the false-positives. For the false-negatives we can check the type's BaseType, looping until we either find a match or run out of types to try.
TypeTransmogrifier
A TypeTransmogrifier is similar to an EnumTransmogrifier but it maps enumeration values to types rather than strings:
Update 2008-06-12: The class is now static and does not wrap an EnumTransmogrifier
.
public static partial class TypeTransmogrifier<T>
{
private static class Null{}
private static System.Type Nullity = typeof(Null) ;
public static readonly System.Type BaseType = typeof(T) ;
public static T DefaultValue ;
private readonly System.Collections.Generic.Dictionary<System.Type,T> types =
new System.Collections.Generic.Dictionary<System.Type,T>() ;
private readonly System.Collections.Generic.Dictionary<T,System.Type> values =
new System.Collections.Generic.Dictionary<T,System.Type>() ;
}
The Null
class is used because null is not allowed as a key in a Dictionary.
The constructor iterates the values of the enumeration, accesses the attributes and populates the Dictionaries.
static TypeTransmogrifier
(
)
{
if ( !BaseType.IsEnum )
{
throw ( new System.ArgumentException ( "T must be an Enum" ) ) ;
}
PIEBALD.Attributes.EnumDefaultValueAttribute.GetDefaultValue<T> ( out DefaultValue ) ;
System.Type atttyp = typeof(PIEBALD.Attributes.TypeTransmogrifierAttribute) ;
System.Type temp ;
foreach
(
System.Reflection.FieldInfo field
in
BaseType.GetFields
(
System.Reflection.BindingFlags.Public
|
System.Reflection.BindingFlags.Static
)
)
{
if ( field.FieldType == BaseType )
{
foreach
(
PIEBALD.Attributes.TypeTransmogrifierAttribute att
in
field.GetCustomAttributes ( atttyp , false )
)
{
if ( att.Type == null )
{
temp = Nullity ;
}
else
{
temp = att.Type ;
}
if ( types.ContainsKey ( temp ) )
{
throw ( new System.ArgumentException
(
"Not all the types are unique."
,
field.Name
) ) ;
}
types [ temp ] = (T) field.GetValue ( null ) ;
values [ types [ temp ] ] = temp ;
}
}
}
return ;
}
The TryParse
method uses the Dictionary of types and also checks the item's BaseType
as needed:
public static bool
TryParse
(
object Subject
,
out T Result
)
{
bool result = false ;
Result = DefaultValue ;
if ( Subject == null )
{
if ( result = types.ContainsKey ( Nullity ) )
{
Result = types [ Nullity ] ;
}
}
else
{
System.Type objtype ;
if ( Subject is System.Type )
{
objtype = (System.Type) Subject ;
}
else
{
objtype = Subject.GetType() ;
}
while ( !result && ( objtype != null ) )
{
if ( result = types.ContainsKey ( objtype ) )
{
Result = types [ objtype ] ;
}
else
{
objtype = objtype.BaseType ;
}
}
}
return ( result ) ;
}
Note that null references will result in a match if the enumeration is set up to allow that. Also, when a System.Type
is passed in it will be used directly.
Update 2008-06-12: Added the following two methods.
Call TryParse
and throw ArgumentException
if it fails.
public static T
Parse
(
object Subject
)
{
T result = DefaultValue ;
if ( !TryParse ( Subject , out result ) )
{
string typ ;
if ( Subject == null )
{
typ = "null" ;
}
else
{
if ( Subject is System.Type )
{
typ = ((System.Type) Subject).FullName ;
}
else
{
typ = Subject.GetType().FullName ;
}
}
throw ( new System.ArgumentException
(
"The supplied type did not translate."
,
typ
) ) ;
}
return ( result ) ;
}
Attempt to get the Type
associated with the enumeration value.
public static System.Type
GetType
(
T Value
)
{
System.Type result = null ;
if ( values.ContainsKey ( Value ) )
{
if ( values [ Value ] != Nullity )
{
result = values [ Value ] ;
}
}
else
{
throw ( new System.ArgumentException
(
"The supplied value did not translate."
,
"Value"
) ) ;
}
return ( result ) ;
}
Using the Code
In the earlier snippet, replace the EnumTransmogrifier
with a TypeTransmogrifier
and pass the item directly to TryParse
:
enum Controls
{
[Description("System.Windows.Forms.Button")]
Button
,
[Description("System.Windows.Forms.TextBox")]
TextBox
...
}
TypeTransmogrifier<Controls> controls = new TypeTransmogrifier<Controls>() ;
void F ( object item )
{
Controls control ;
if ( controls.TryParse ( item , out control ) )
{
switch ( control )
{
case Controls.Button : ...
case Controls.TextBox : ...
}
}
}
TypeDemo.cs
The included TypeDemo.cs file uses the following enumeration:
public enum WinForms
{
[PIEBALD.Attributes.TypeTransmogrifierAttribute(null)]
Null
,
[PIEBALD.Attributes.TypeTransmogrifierAttribute(typeof(System.Windows.Forms.Control))]
Control
,
[PIEBALD.Attributes.TypeTransmogrifierAttribute(typeof(System.Windows.Forms.ButtonBase))]
ButtonBase
,
[PIEBALD.Attributes.TypeTransmogrifierAttribute(typeof(System.Windows.Forms.Button))]
Button
} ;
And defines the following classes which could cause false-positives and false-negatives with other techniques:
private class Button {}
private class MyButton : System.Windows.Forms.Button {}
The demo will simply show the results of the TryParse
:
private static void
Show
(
object What
)
{
bool res ;
WinForms typ ;
res = PIEBALD.Types.TypeTransmogrifier<WinForms>.TryParse ( What , out typ ) ;
System.Console.WriteLine ( "{0} {1}" , res , typ ) ;
return ;
}
Show ( null ) ;
Show ( new System.DateTime() ) ;
Show ( new System.Windows.Forms.Button() ) ;
Show ( new System.Windows.Forms.RadioButton() ) ;
Show ( new System.Windows.Forms.TextBox() ) ;
Show ( new System.Web.UI.WebControls.Button() ) ;
Show ( new Button() ) ;
Show ( new MyButton() ) ;
Show ( typeof(MyButton) ) ;
Show ( (new MyButton()).GetType() ) ;
History
2008-06-10 First submitted
2008-06-12 Made it static, added use of TypeTransmogrifierAttribute
(at leppie's suggestion), added Parse
and GetType