|
Using Generics and Inheritance to Simplify Development
|
by Brian Mains
Feedback
|
Average Rating:
Views (Total / Last 10 Days):
35632/
50
|
|
|
Introduction |
Inheritance is a common Object Oriented Programming (OOP) concept
that can be both well used and overused in application development. In
combination with Generics, a new feature in .NET 2.0 that supports strong
typing, using both inheritance and generics, can be a powerful combination
helping to reduce the total amount of coding effort you do in application
development.
|
Development Styles |
Each developer has a different style of developing applications.
Given a single software problem, many people will come up with many different
ways to develop the solution. However, analyzing your trends in software
development can help to create a base of reusable code that will minimize your
total development efforts.
In addition, some of the standard tasks that you see
examples on the MSDN can use a combination of inheritance and generics to help
you not have to rewrite any of that code at all, as we will see in a future
example.
|
Beware the Pitfalls |
Before looking into the benefits, beware the potential
pitfalls with inheritance and generics in combination with inheritance.
Inheritance can and sometimes does get out of hand, where a derived class may
derive from five or more base classes. While it may pay off for some people,
it can quickly become a maintenance nightmare. What if a property in the third
base class changes, but should not change for the two-three other derived
classes? The solution may be to ignore the property or to shadow it and make
it private. What happens if a method changes its algorithm? Will it break the
inheritance chain or cause a problem for the other classes?
Sometimes, a different development structure (maybe the
state/strategy pattern or simplifying the inheritance structure) is a better
solution. I know from personal experience the problems that can occur from
deep chains of inheritance, as I have developed programs with the very problem
I am talking about. Note, however, I am not stating that inheritance is bad,
as we shall soon see; I am just warning you to be careful how you plan your
inherited structures and recommend keeping your inheritance chains short.
Generic base classes can cause a problem because the generic
type of a base class makes the base class specific and you cannot take
advantage of generic switching in some situations. For instance, a class
Entity that inherits from BusinessObject<EntityBase> does not inherit
directly from BusinessObject, but BusinessObject<EntityBase> and,
therefore, you may not be able to reference it dynamically in your application.
|
The Benefits of Inheritance |
Inheritance is not bad as I have stated before. There are
plenty of good designs that involve inheritance. Everything inherits something
anyway. If there is no direct inheritance, then it inherits from
System.Object.
There are many benefits to inheritance. For instance, it
makes it easy to implement list or dictionary collections, using the
List<T> or Dictionary<K,V> classes. In addition, inheritance makes
it easier to add custom functionality to the base object. For instance, one
can inherit from the GridView or DetailsView class and add additional
functionality to it.
|
Example: Generic Event Arguments |
To make it easy to add a generic single parameter argument
for an application, the following event argument was created.
Listing 1
namespace Nucleo.EventArguments
{
public class DataEventArgs < T > : EventArgs
{
private T _data;
public T Data
{
get
{
return _data;
}
}
public DataEventArgs(T data)
{
_data = data;
}
}
public delegate void DataEventHandler < T > (object sender, DataEventArgs < T
> e);
}
This event argument makes it simplistic to expose a single
strongly-typed event argument property in an application. Below are some of
the examples of the consumption of the delegate.
Listing 2
public static event DataEventHandler<Document> DocumentClosed;
public static event DataEventHandler<Document> DocumentCreated;
public static event DataEventHandler<Document> DocumentSelected;
These delegates are defined in a static class and any class
that references these events will have a DataEventArgs object that has a
Document type.
|
Example: Custom String Collection |
The built-in StringCollection, defined in the .NET
Framework's System.Collections.Specialized namespace, is easy to work with
strings, but I wanted the ability to convert a list to/from a string. Take for
instance a comma-separated value string. I wanted the list to have the ability
to convert the comma-separated list to/from a string. I also wanted to work
with other separators as well, such as the vertical bar/pipe (|) or tab (\t),
which you normally see data separated by. Below, an inherited structure adds
these features.
Listing 3
namespace Nucleo.Collections
{
public class StringCollection: StringCollection
{
public void FromCommaSeparatedList(string text)
{
this.FromSeparatedList(text, ",");
}
public void FromSeparatedList(string text, string separator)
{
string[]items = text.Split(separator.ToCharArray());
foreach (string item in items)
{
if (!string.IsNullOrEmpty(item))
this.Add(item);
}
}
public string ToCommaSeparatedList()
{
return ToSeparatedList(",");
}
public string ToSeparatedList(string separator)
{
string list = string.Empty;
for (int i = 0; i < this.Count; i++)
{
if (i != 0)
list += separator;
list += this[i];
}
return list;
}
public static StringCollection FromList(string text, string separator)
{
StringCollection collection = new StringCollection();
collection.FromSeparatedList(text, separator);
return collection;
}
}
}
Note that the class also uses a static method to create a
collection from a list string, using a separator defined by the user. This
class now has the ability to convert a string from/to CSV or other formatted
data.
|
Example: Custom Configuration Collections |
I first read the MSDN
example on custom configuration collections and saw what was involved in
creating a custom collection from the example. Maybe there is an easier way,
but that is where generics and inheritance come into play. Instead of
implementing that approach every time, I created this element class:
Listing 4
namespace Nucleo.Configuration
{
public abstract class ConfigurationElementBase:
System.Configuration.ConfigurationElement
{
protected internal abstract string Key
{
get;
}
}
}
The key property is something that must be overridden and
returns a unique value that identifies an element in a collection. The key is important
because the base collection for configuration collections uses a unique value.
The key property ensures this. Because the collection knows of the Key
property in the element base interface, the collection can automatically use
the element without having to create a custom collection on your own. The
following class is the base collection class, which defines a generic property
that references the element base class above.
Listing 5
namespace Nucleo.Configuration
{
public abstract class ConfigurationCollectionBase < T > :
ConfigurationElementCollection where T: ConfigurationElementBase
{
public override ConfigurationElementCollectionType CollectionType
{
get
{
return ConfigurationElementCollectionType.AddRemoveClearMap;
}
}
public T this[int index]
{
get
{
return (T)this.BaseGet(index);
}
set
{
if (this.BaseGet(index) != null)
this.BaseRemoveAt(index);
this.BaseAdd(index, value);
}
}
new public T this[string key]
{
get
{
return (T)this.BaseGet(key);
}
}
public void Add(T element)
{
this.BaseAdd(element);
}
public void Clear()
{
this.BaseClear();
}
public bool Contains(string key)
{
return (this.BaseGet(key) != null);
}
protected override ConfigurationElement CreateNewElement()
{
return Activator.CreateInstance < T > ();
}
protected override object GetElementKey(ConfigurationElement element)
{
return ((T)element).Key;
}
public void Remove(T element)
{
this.Remove(element.Key);
}
public void Remove(string key)
{
this.BaseRemove(key);
}
public void RemoveAt(int index)
{
this.BaseRemoveAt(index);
}
}
}
Now, because the work is done in the base collections class,
inheritance from this is a snap and reduces the amount of code needed. For
instance, below is a derived implementation of the element.
Listing 6
namespace Nucleo.Web.Configuration
{
public class IndividualPageElement: ConfigurationElementBase
{
protected override string Key
{
get
{
return this.PageName;
}
}
}
}
The collection definition is even easier.
Listing 7
public class IndividualPageElementCollection :
ConfigurationCollectionBase<IndividualPageElement>
{
}
That is all it takes to implement a collection with all that
functionality and with strong typing. This is a more useful example because it
really can cut down on the total amount of coding that is required for a
collection. However, this usage is not limited to collections, but can be used
in many scenarios.
|
Example: Custom Data Control Fields |
The DataControlField base class is a class used for the
columns in a GridView and fields in a DetailsView control. These fields
provide the interface for a data bound field provided from a data source
control or other type of data source.
The first level of data field base controls is the
BaseDataField class.
Listing 8
namespace Nucleo.Web.Controls
{
public abstract class BaseDataField: DataControlField
{
private bool _insertMode = false;
public string DataField
{
get
{
object o = ViewState["DataField"];
return (o == null) ? string.Empty : o.ToString();
}
set
{
ViewState["DataField"] = value;
}
}
protected bool InsertMode
{
get
{
return _insertMode;
}
}
public bool ReadOnly
{
get
{
object o = ViewState["ReadOnly"];
return (o == null) ? false : (bool)o;
}
set
{
ViewState["ReadOnly"] = value;
}
}
protected T ExtractValueFromControl < T > (TableCell cell, int index)where
T: Control
{
if (index < 0 || index >= cell.Controls.Count)
throw new ArgumentOutOfRangeException("index", Errors.OUT_OF_RANGE);
Control control = cell.Controls[index];
if (control == null)
throw new InvalidOperationException(Errors.CANT_EXTRACT_CONTROL_IN_CELL)
;
return (T)control;
}
public override void ExtractValuesFromCell(IOrderedDictionary dictionary,
DataControlFieldCell cell, DataControlRowState rowState, bool
includeReadOnly)
{
_insertMode = (rowState == DataControlRowState.Insert);
}
protected T GetDataItemValue < T > (object container, string property)
{
if (container == null || this.DesignMode)
return default(T);
object dataItem = DataBinder.GetDataItem(container);
if (dataItem == null)
return default(T);
object value = DataBinder.GetPropertyValue(dataItem, property);
if (value != null)
return (T)value;
else
return default(T);
}
protected bool IsReadMode(DataControlRowState rowState)
{
return (this.ReadOnly || rowState == DataControlRowState.Normal ||
rowState == DataControlRowState.Alternate || rowState ==
DataControlRowState.Selected);
}
}
}
There are several standard fields when implementing a data
field. The DataField, InsertMode, and ReadOnly properties all have a specific
function when rendering a data control field. InsertMode is based off of the
DataControlRowState setting of Insert specified from the ExtractValuesFromCell
method. DataField is commonly used because only one field is specified; there
are alternatives if the data field uses multiple data fields.
To further minimize the amount of work to do, I created
another base class.
Listing 9
namespace Nucleo.Web.Controls
{
public abstract class BaseCustomDataField: BaseDataField
{
public abstract void BindEditControl(object control, bool insertMode);
public override void ExtractValuesFromCell(IOrderedDictionary dictionary,
DataControlFieldCell cell, DataControlRowState rowState, bool
includeReadOnly)
{
base.ExtractValuesFromCell(dictionary, cell, rowState, includeReadOnly);
string value = null;
if (cell.Controls.Count > 0)
value = this.GetEditControlValue(cell);
//If the dictionary contains the data field, overwrite the value
if (dictionary.Contains(this.DataField))
dictionary[this.DataField] = value;
//Create a new dictionary entry
else
dictionary.Add(this.DataField, value);
}
public abstract string GetEditControlValue(TableCell cell);
public override void InitializeCell(DataControlFieldCell cell,
DataControlCellType cellType, DataControlRowState rowState, int rowIndex)
{
base.InitializeCell(cell, cellType, rowState, rowIndex);
Control control = null;
if (cellType == DataControlCellType.DataCell)
{
if (this.IsReadMode(rowState))
control = cell;
else
{
cell.Controls.Add(this.SetupEditControl());
if (!string.IsNullOrEmpty(this.DataField))
control = cell.Controls[0];
}
if (control != null && this.Visible)
control.DataBinding += new EventHandler(control_DataBinding);
}
}
public abstract Control SetupEditControl();
void control_DataBinding(object sender, EventArgs e)
{
if (sender is TableCell)
{
TableCell cell = sender as TableCell;
cell.Text = this.GetDataItemValue < string > (cell.NamingContainer);
}
else
this.BindEditControl(sender, this.InsertMode);
}
}
}
This class makes use of the Template Method pattern, meaning
that any derived class only has to override the BindEditControl,
GetEditControlValue, and SetupEditControl methods. SetupEditControl handles
creating the control, BindEditControl handles the binding, and
GetEditControlValue handles getting the value specified when in edit mode. From
this, it makes it even simpler to use the base controls. The InitializeCell
and ExtractValuesFromCell methods all have a standardly-used definition that
makes use of BindEditControl, GetEditControlValue, and SetupEditControl
methods. And if that does not meet your needs, you can choose a lesser
implementation from BaseDataField class. Below are the implementations of
those methods in a TextField class. Note that the properties are omitted.
Listing 10
public class TextField: BaseCustomDataField
{
protected override DataControlField CreateField()
{
return new TextField();
}
public override void BindEditControl(object control, bool insertMode)
{
TextBox box = control as TextBox;
//If not in insert mode, set the text property
if (!insertMode)
box.Text = this.GetDataItemValue < string > (box.NamingContainer);
}
public override string GetEditControlValue(TableCell cell)
{
return this.ExtractValueFromControl < TextBox > (cell).Text;
}
public override Control SetupEditControl()
{
TextBox box = new TextBox();
box.TextMode = this.TextMode;
box.Rows = this.Rows;
box.Columns = this.Columns;
box.MaxLength = this.MaximumLength;
box.ToolTip = this.HeaderText;
return box;
}
}
|
Conclusion |
You have seen several examples of inheritance and generics
and how you can use it in custom development, whether it is business systems or
web/windows applications. These examples were taken from the Nucleo Framework I am
developing.
|
|
|
|
|
|