You have probably not escaped seeing the latest commercials for Microsoft
Windows Server 2003, which urge listeners to "do more with less"; this has
been an aim of software engineering since the very beginning.
When I started writing software using C and C++ on Unix systems,
programmers aimed to do more with less by reusing others' header files and
precompiled libraries.
Today I find myself doing more with less through many technologies such
as COM+ components and commercial extensions to the .NET Framework.
Recently I discovered I could also do more with less through .NET
Reflection and dynamically assigning controls to ASP.NET pages and panels.
This article will present an introduction to the technologies and
techniques that I've discovered over the past 18 months and that
ultimately resulted in the epiphany that has empowered me to do more with
less. This article will present an introduction to the technologies and techniques that I've discovered over the past 18 months - and that ultimately resulted in the epiphany that has empowered me to do more with less. The source code for this article can be downloaded from http://www.sys-con.com/sourcec.cfm.
Introduction to Reflection
Unlike unmanaged environments, in which the source code you write is
ultimately compiled directly into native machine code for the specific
microprocessor upon which it is intended to run, Microsoft .NET code is
compiled into Microsoft Intermediate Language (also known as MSIL, or simply
IL).
Any Common Language Specification (CLS)compliant language can create
IL, which the Common Language Runtime (CLR) can then run through a process
known as just-in-time compilation to convert the IL (such as that shown in
Listing 1) to machine code native to the processor on which the CLR is
running. The source code for this article can be downloaded from
below.
In addition to the IL generated by a managed compiler, .NET assemblies
also contain metadata (data about data). Within an assembly metadata is a
system of descriptors that describe the structural items of an application,
e.g., the members and attributes of a given class.
Using the classes within the System.Reflection namespace you can access
the information held within an assembly's metadata dynamically at runtime.
private static System.Type m_hashtable = null;
private static System.Type[] m_interfaces = null;
In the preceding C# code I have defined two variables: m_hashtable of
type System.Type, and an array m_interfaces, also of type System.Type.
m_hashtable = System.Type.
GetType("System.Collections.Hashtable");
System.Type.GetType is a static method that returns a System.Type based
on the supplied case-sensitive, fully qualified class name. Another way of
doing this would be to use the C# type of operator as shown below:
m_hashtable = typeof(System.Collections.Hashtable);
System.Type includes many methods that can be used to discover
information about the current System.Type object. One such method,
GetInterfaces(), returns an array of System.Type objects representing all
the interfaces implemented or inherited by the current type. Using
GetInterfaces(), we now assign the array of System.Type objects representing
the interfaces implemented or inherited for the Hashtable class.
m_interfaces = m_hashtable.GetInterfaces();
Compared to its predecessors, the C and C++ languages, one of the best
additions to the C# language is the foreach loop, which we use now to print
out the names of all the interfaces discovered by calling GetInterfaces():
foreach(System.Type t in m_interfaces)
{
Console.WriteLine("{0} Realizes {1}",
m_hashtable.Name, t.Name);
}
If you run this code you will see the following output:
Hashtable Realizes IDictionary
Hashtable Realizes ICollection
Hashtable Realizes IEnumerable
Hashtable Realizes ISerializable
Hashtable Realizes IDeserializationCallback
Hashtable Realizes ICloneable
Reflection enables you to produce highly adaptable dynamic applications.
However, the very dynamic nature of using Reflection effectively requires
you to program defensively.
Imagine the following scenario: you have defined a new interface,
ISearchable, and wish to have your code act upon a type differently if the
type supports the ISearchable interface. You then deploy your application,
which functions without error for many months until a user installs a newer
version of the .NET Framework.
It appears that Microsoft has been kind enough to implement their own
ISearchable interface (highly possible), which causes your code to throw
exceptions of type System.MissingMethodException when the code runs.
Therefore, if you ever write code such as this:
if(type.Name == "ISearchable")
{
}
you might want to consider changing it to behave a little more defensively
against changes outside of your control:
if(type.Name == "ISearchable" && type.Namespace ==
"PrecisionObjects.Interfaces")
{
}
It is highly unlikely that Microsoft will implement an interface ISearchable
in the namespace PrecisionObjects.Interfaces.
Dynamically Adding Controls to ASP.NET Web Forms
ASP.NET allows you to dynamically add controls to Web Forms using the
Controls property of container controls such as the System.Web.UI.WebControls.Panel control.
Panel has this ability because it directly inherits from the ASP.NET
base control class System.Web.UI.Control, which provides the Controls property.
ASP.NET System.Web.UI.Page also inherits from System.Web.UI.Control and therefore also contains a Controls collection property.
However, any controls added directly to the Controls collection of the page
will not be rendered, and an exception of type System.Web.HttpException will be thrown when you attempt to run your code. This is demonstrated in Listing 2. Listing 3 successfully adds the new control to a System.Web.UI.WebControls.Panel control.
ASP.NET allows the developer to add controls dynamically to many other
container controls. You will probably find it useful to dynamically add
controls to an ASP.NET table control.
You can add controls to both the System.Web.UI.WebControls.Table
Cell and the System.Web.UI.WebControls.TableRow controls. I'll show you how in the SearchPage example, which dynamically adds controls using information discovered through
Reflection about another class.
Using Reflection to Empower Dynamic Control Creation
I have now introduced two techniques that are very powerful in their own
right but become even more powerful when combined to build dynamic ASP.NET
Web Forms or user controls.
If you have spent any time writing ASP.NET Web applications you will
have undoubtedly discovered the usefulness of ASP.NET user controls (*.ascx
controls). If you haven't yet, then you might be convinced of their
usefulness after seeing the following example.
Imagine your enterprise application has a Search page that contains
three System.Web.UI.WebControls.Panel controls: Search-Panel, ResultsPanel, and DetailsPanel.
SearchNavigation.ascx (see Listings 4 and 5) allows you to dynamically,
through Reflection, establish a hyperlink menu for each of the panels on a
Web Form and then allow the user to set the appropriate Panel.Visible
property to true and all others to false.
If any further panels are subsequently added to the Web Form, the code
within SearchNavigation.ascx doesn't have to be updated because it will
dynamically discover the new panel at runtime.
Before we can discover the panels that exist within the System
.Web.UI.Page control collection we need to retrieve the type of the page:
m_type = typeof(SearchPage);
Just as we used GetInterfaces() before to retrieve the interfaces
implemented or inherited for the System.Collections.Hashtable class, we can
now use GetFields() to retrieve the fields of the current type, in this case
the SearchPage.
FieldInfo[] fields =
m_type.GetFields(BindingsFlags.NonPublic
| BindingsFlags.Instance);
Now we can use the C# foreach loop again to iterate through the returned
fields to search for System.Web.UI.WebControls.Panel controls.
foreach(FieldInfo field in fields)
{
if(field.FieldType.FullName ==
"System.Web.UI.WebControls.Panel")
{
}
}
We can create an instance of the System.Web.UI.WebControls.LinkBut
ton class for each panel found on the Web Form, while also dynamically
adding an event handler for the link button's Click event (see Listing 6).
Finally, we must add the newly created TableRow object to the ASP.NET
table control:
CommandTable.Rows.Add(_row);
We have now created a user control that can be used to build a menu of
ASP.NET panels on any Web Form. To use the control, all you would need to
change is the initial type assigned to m_type within the code; even this
could be automated to produce a truly disconnected user control.
As you can see in Figure 1, the link buttons were successfully created
for the three panels on the Search page, allowing the user to click between
the panels. The event handler will activate the appropriate panel when the
user selects a menu item.
The controls on the Search panel are also dynamically constructed at
runtime using Reflection and custom attributes.
Custom Attributes
Custom attributes are another tool available to developers using the
.NET Framework; they allow you to insert additional metadata into your
assembly that can then be accessed through Reflection.
An example of a custom attribute is the System.Web.Services.WebMethodAttribute class, which you are probably more used to seeing within ASP.NET Web services as [WebMethod].
SearchPage.aspx uses another custom attribute class, SearchableAttribute, to distinguish between members of the Publisher class that
are searchable and those that are not. The code for SearchableAttribute is
shown in Listing 7.
You will notice the use of another attribute in the above code;
[AttributeUsage] is used to define where a custom attribute can be used. In
the case of [Searchable] we want it to be applied only to properties defined
in classes written in a CLS-compliant language. Therefore, we use the
AttributeTargets enumeration to specify this.
SearchPage.aspx
While working on a recent ASP.NET project that involved constructing
several different search pages for various entities within the system I
began thinking about a way to construct a single search page that would
dynamically construct the Search, Results, and Details panels at runtime,
enabling me to write a single dynamic search page.
Using the custom attribute [Searchable] it is possible to define in
metadata which properties of a given class will be searchable through the
dynamic search page. Listing 8 shows the [Searchable] attribute used in the
class Publisher.
SearchPage.aspx uses the metadata injected into the Publisher assembly
through the [Searchable] attribute to determine which properties of the
Publisher class should be displayed within the Search-Panel.
As within the SearchNavigation.ascx user control, we start by assigning the type of the target class (in this case Publisher) to a variable of type System.Type and then get the associated interfaces to ensure that the class realizes the ISearchable
interface (see Listing 9).
Assuming that we discover that Publisher does indeed realize the
ISearchable interface and that ISearchable does reside within the namespace
PrecisionObjects.Interfaces, we can then check which properties of Publisher we should allow our user to search upon.
We start by getting an array of type PropertyInfo, which contains all of
the properties for a given type. Again, we can use the C# foreach loop to
iterate through the properties and dynamically add the associated label and
textbox controls to the SearchPanel.
Listing 10 shows how you can then establish whether or not a given
property is adorned with the [Searchable] attribute. I have removed the code
to add the controls to the SearchPanel, as it is nearly identical to the
code presented previously in constructing the SearchNavigation.ascx user
control.
Conclusion
Reflection continues to provide me with an excellent mechanism to
produce highly dynamic and reusable code and does enable me to do more
with less.
Some of you reading this have probably been thinking that there must be
a relative degradation in performance when using Reflection and,
unfortunately, all good things do come at a price. However, the performance
impact of using Reflection is relatively minor; in fact many aspects of the
.NET Framework and Visual Studio .NET wouldn't be possible without it.
About The Author
Doug Holland, a professional .NET consultant based in Roseville, CA, has over eight years of experience in designing and developing software. Doug currently works with the .NET Framework and specializes in consulting for companies using Rational XDE for .NET development.
doug.holland@precisionobjects.com
Listing 1: HelloWorld implemented directly using MSIL
.assembly HelloWorld { }
.assembly extern mscorlib { }
.method public static void main() {
.entrypoint
ldstr "Hello .NET World"
call void
[mscorlib]System.Console::WriteLine
(Class System.String)
ret
}
Listing 2: Adding controls directly to a System.Web.UI.Page controls
collection causes a System.Web.HttpException to be thrown
/// <summary>
/// System.Web.HttpException thrown when attempt
/// ing to add controls
/// dynamically to the controls collection of a
/// System.Web.UI.Page
/// directly. Therefore we must add them to a
/// Panel instead.
/// </summary>
private void Page_Load(object sender, EventArgs e)
{
try
{
System.Web.UI.WebControls.TextBox _textbox
= new System.Web.UI.WebControls.TextBox();
textbox.Text = "Dynamically Inserted TextBox";
this.Controls.Add(_textbox); // will not render textbox!!!!
}
catch(System.Web.HttpException ex)
{
// System.Web.HttpException will occur if you
// add controlsdirectly to the
// System.Web.UI.Page controls collection.
}
catch(Exception ex)
{
}
}
Listing 3: Using a System.Web.UI.WebControls.Panel to add controls dynamically
protected System.Web.UI.WebControls SearchPanel
= new System.Web.UI.WebControls();
/// <summary>
/// Creates a TextBox control dynamically and
/// successfully adds it
/// to the SearchPanel controls collection.
/// </summary>
private void Page_Load(object sender, EventArgs e)
{
try
{
System.Web.UI.WebControls.TextBox _textbox
= new System.Web.UI.WebControls.TextBox();
_textbox.Text = "Dynamically Inserted TextBox";
SearchPanel.Controls.Add(_textbox);
}
catch(System.Web.HttpException ex)
{
// What no worries ;)
}
catch(Exception ex)
{
// What no worries ;)
}
}
Listing 4: SearchNavigation.ascx Page_Load Event Handler
private System.Type m_type = null;
private void Page_Load(object sender, EventArgs e)
{
try
{
m_type = typeof(SearchPage);
FieldInfo[] fields =
m_type.GetFields(BindingsFlags.NonPublic |
BindingsFlags.Instance);
TableRow _row = new TableRow();
foreach(FieldInfo field in fields)
{
if(field.FieldType.FullName
== "System.Web.UI.WebControls.Panel")
{
LinkButton _linkButton = new LinkButton();
linkButton.Text = field.Name.Replace("Panel",
string.Empty);
_linkButton.ForeColor = Color.FromArgb(99,
99, 99);
_linkButton.Click
+= new EventHandler(this.LinkButton_Clicked);
TableCell _cell = new TableCell();
_cell.Controls.Add(_linkButton);
_row.Controls.Add(_cell);
}
}
}
catch(Exception ex)
{
// What no worries ;)
}
}
Listing 5: SearchNavigation.ascx LinkButton_Clicked Event Handler
private void LinkButton_Clicked(object sender, EventArgs e)
{
try
{
foreach(System.Web.UI.Control
_control in this.Parent.Controls)
{
if(_control.Type ==
typeof(System.Web.UI.WebControls.Panel))
{
if(_control.ID ==
((LinkButton)sender).Text
+ "Panel")
_control.Visible =
true;
else
_control.Visible =
false;
}
}
}
catch(Exception ex)
{
// What no worries ;)
}
}
Listing 6
foreach(FieldInfo field in fields)
{
if(field.FieldType.FullName ==
"System.Web.UI.WebControls.Panel")
{
LinkButton _linkButton = new
LinkButton();
_linkButton.Text = field.Name.Re
_place("Panel", string.Empty);
_linkButton.ForeColor = Color.From
Argb(99, 99, 99);
_linkButton.Click
+= new EventHandler(
this.LinkButton_
Clicked);
TableCell _cell = new
TableCell();
_cell.Controls.Add(_linkButton);
_row.Controls.Add(_cell);
}
}
Listing 7: SearchAttribute.cs
using System;
namespace PrecisionObjects.CustomAttributes
{
/// <summary>
/// class SearchableAttribute
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class SearchableAttribute : Attribute
{
private string m_validation;
public SearchableAttribute(string validation)
{
m_validation = validation;
}
}
}
Listing 8: Publisher.cs
using System;
using PrecisionObjects.CustomAttributes;
using PrecisionObjects.Interfaces;
namespace PrecisionObjects.Organizations
{
public class Publisher : ISearchable
{
private string m_name;
private string m_city;
private string m_state;
private string m_country;
private string m_contact;
/// <summary>
/// public string Name ~ gets / sets the
/// publisher name
/// </summary>
[Searchable("Insert validation
expression")]
public string Name
{
get
{
return m_name;
}
set
{
m_name = value;
}
}
/// <summary>
/// public string City ~ gets / sets the
/// publisher city
/// </summary>
[Searchable("Insert validation
expression")]
public string City
{
get
{
return m_city;
}
set
{
m_city = value;
}
}
/// <summary>
/// public string State ~ gets / sets the
/// publisher state
/// </summary>
[Searchable("Insert validation
expression")]
public string State
{
get
{
return m_state;
}
set
{
m_state = value;
}
}
/// <summary>
/// public string Country ~ gets / sets
/// the publisher country
/// </summary>
[Searchable("Insert validation
expression")]
public string Country
{
get
{
return m_country;
}
set
{
m_country = value;
}
}
/// <summary>
/// public string Contact ~ gets /
/// sets the publisher contact
/// </summary>
public string Contact
{
get
{
return m_contact;
}
set
{
m_contact = value;
}
}
}
}
Listing 9
=m_type = typeof(Publisher);
// establish if m_type implements ISearchable
if(m_type.IsClass && !m_type.IsAbstract)
{
m_interfaces = m_type.GetInterfaces();
foreach(System.Type type in m_interfaces)
{
if(type.Name == "ISearchable"
&&
type.Namespace ==
"PrecisionObjects.
Interfaces")
{
Listing 10
PropertyInfo[] props = m_type.GetProperties();
foreach(PropertyInfo p in props)
{
string name = p.Name;
object[] supported
= p.GetCustomAttributes(typeof
(SearchableAttribute), false);
if(supported.Length != 0) // Property Is
Searchable
{
// Dynamically add associated controls
// to ASP.NET table
}
}
All Rights Reserved
Copyright © 2004 SYS-CON Media, Inc.
E-mail:
info@sys-con.com