In-Depth
Simplify Your Code with Reflection
Take advantage of generics and reflection to determine whether a user has changed any data on a given form.
Technology Toolbox: VB.NET, SQL Server 2005 SP2, Note: This article uses VB.NET for its samples, but you can find C# and VB.NET versions of the code for this article online.
Some programming issues are universal. For example, every developer must deal with the question of whether a user has changed any values on a given form, for the purpose of determining whether the data must be saved back to the database. There's no point saving data back if it hasn't changed. And like many universal problems, there are many ways to implement a solution. You can trap keystrokes, use an "extender control," check the original data against the changed data, and so on.
My own preference is to use a data-driven approach and reflection. Combining these two paradigms lets you add and check new controls quickly and easily. This approach also gives you a single method that you can use for all your applications, regardless of how many different third-party controls you use. Reflection is needed in this situation because the control and property values are stored in an XML file, and you need to be able to read a property dynamically at runtime. This would be impossible to do without reflection.
Before I walk you through a data-driven approach that relies on reflection, it may be helpful to look at another popular approach, where you use an extender control. For example, consider what happens when you write a class that checks whether a value has changed in a control: You end up with a large Select Case/switch statement. Adding a third-party control requires that you add a new Case statement and add an additional enumeration. This involves recompiling your code and putting the new DLL into your project. It's an approach that can work for your project because you referenced the third-party component, but it presents a problem when someone else wishes to use your class, and they don't have that third-party component (or its license). What you end up with is a class that must be customized for each project you develop, which can result in a maintenance nightmare. A better approach for this scenario would be to develop code that doesn't use a Select Case/switch statement--one that would allow you to add new components to each project without recompiling your DLL. Another issue with the extender control approach is that each control has a different property that you need to check for changed data, which can make it difficult to use a one-size-fits-all type of coding technique.
But you can overcome all of these shortcomings using reflection (see "To Reflect or Not To Reflect,"). Reflection gives you the ability to create just one DLL, and then use that DLL in many projects, as well as add new controls without having to recompile your app. The approach I'll describe in this article is to check all controls at once, rather than use an extender control where you check each control individually. If you prefer the latter approach, it would be easy for you to modify the solution described so it works on a control-by-control basis.
Define Your Process
Adopting this approach means that you need a way to store a list of each control type, as well as the property for each control type that will be used to read the data in each of your controls. You also need to keep a couple of things in mind when dealing with all the different types of controls that you might come across. For a TextBox or a CheckBox control, you can read the Text property or Checked properties, respectively. However, a ListBox control requires that you check to see whether it's a multiple-selection list box. If it is, you must get all selected values, as opposed to just one. If you're using a ComboBox, you determine whether the data has changed by choosing either the SelectedIndex property or the Text property, depending on the DropDownStyle property. For example, a MonthCalendar control has two properties that can change: SelectionStart and SelectionEnd.
The reflection-based approach requires that you handle a few different situations when working with the various controls (see Table 1). Once you have a firm grasp of these scenarios, you're ready to create the data in an XML file (see Listing 1).
Generics can help you get this XML file into a useable form in your applications. You create two classes to work with this XML data. The first class contains one property for each XML element you see in the XML file described in Listing 1. Call this class DirtyControl:
Public Class DirtyControl
Private mstrType As String
Private mstrProperty1 As String
Private mstrProperty2 As String
Private mstrPropertyToCheck As String
Private mstrPropertyToCheckValue As String
Private mboolIsMultiList As Boolean
Public Property Type() As String
... ' Get/Set code here
End Property
' The rest of the property definitions go here
...
End Class
Next, create a DirtyControl generic list Collection class (Listing 2). Once you have the controls' data loaded into a generic class, you can build a base form class that all your Windows Forms can inherit from:
Public Class WinFormBase
Inherits System.Windows.Forms.Form
Private mintInitialHash As Integer
Public Sub InitControls()
' Set the Initial Hash Value
mintInitialHash = _
GetControls( _
Me.Controls).GetHashCode()
End Sub
Public Function IsDirty() As Boolean
Dim intHash As Integer
' Get the Ending Hash Value
intHash = GetControls( _
Me.Controls).GetHashCode()
Return (mintInitialHash <> intHash)
End Function
... ' More Code Here
End Class
Note the initial declaration of the WinFormBase class. You need one private variable to hold on to the initial hash value that indicates the state of the controls when the form loads.
You load this value by calling the InitControls method from your form once your data is loaded into all the controls. You call the IsDirty method when you're ready to check whether the form is dirty. The base form class compares the two hash values; the form is dirty if they are different.
The InitControls and IsDirty methods both call the method GetControls (see Listing 3). This method uses an instance of the DirtyControls class, and the instance is held in a Shared/static property of a class named AppConfig.
The GetControls method concatenates all the data values from all the controls to form one long string. The InitControls and IsDirty methods then use the GetHashCode() method of the String class to get a unique hash value.
The GetMultiListValues method loops through all the selected indices in a list box and appends the selected data together into a string. It returns this string to be concatenated to the other control values. You can examine this code in the sample here.
Retrieve the Data
The GetControlValue method retrieves the data from the appropriate property on the control:
Private Function GetControlValue( _
ByVal ctl As Control, _
ByVal PropertyName As String) As String
Dim t As Type
t = ctl.GetType()
' Use Reflection to get the property value
Return Convert.ToString(t.InvokeMember( _
PropertyName, BindingFlags.DeclaredOnly Or _
BindingFlags.Public Or _
BindingFlags.NonPublic Or _
BindingFlags.Instance Or _
BindingFlags.GetProperty, _
Nothing, ctl, Nothing))
End Function
Pay special attention to the use of the InvokeMember method of the Type class to call the property name that's passed in. This method is the heart of this solution because this is where you use reflection to retrieve the value from each control in a generic way. For example, you might call this method using this short code fragment:
strValue = GetControlValue( _
txtLastName, "Text")
The syntax required is different, but it might help your understanding of what's going on here to know that the previous code snippet is equivalent to this more traditional line of code:
strValue = txtLastName.Text
So far you've done a lot of the grunt work required to make this solution work; the next step is to initialize your controls on the form. When you add a new Windows Form to your project that you want to perform dirty checking on, you need to change the line in the designer where your Windows Form inherits from the System.Windows.Forms.Form class.
You replace inheriting from the Form class with inheriting from the WinFormBase class. Once you do that, you need to call the InitControls method on the base form class after you load any initial data into your controls.
This sets the InitialHash code for all the controls' values; you'll check these values later to see whether any of the controls' values have changed:
Private Sub frmVersion2_Load(ByVal sender _
As System.Object, _
ByVal e As System.EventArgs) _
Handles MyBase.Load
' Load any data into your controls first...
' Initialize the Dirty Hash Code
MyBase.InitControls()
End Sub
After your user finishes typing in all of their changes and hits the Submit or Save button, it's time to decide whether you need to save the data. Call the IsDirty method on the base form, and it will tell you whether any of the controls have been changed:
Private Sub btnSubmit_Click( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles btnSubmit.Click
If MyBase.IsDirty() Then
MessageBox.Show("Is Dirty")
Else
MessageBox.Show("Is NOT Dirty")
End If
End Sub
The AppConfig class enables you to keep an instance of the DirtyControls list in memory while the application is running. You probably have a class like this already, so you might want to augment it with a DirtyControlsList class that's of the type DirtyControls and a LoadDirtyControls method that loads the DirtyControls.xml file. Next, populate the DirtyControls collection. Be sure to download the online code for this article, where you can check out the source code for this kind of class.
Using reflection and generics in .NET 2.0 can simplify code that was written as recently as a couple of years ago. In the process, you'll end up with more generic code that can be reused much more easily. Making things data-driven also makes the component shown in this article much easier to extend as you add new controls to your projects. You no longer need recompile and redistribute a DLL; instead, all you must do is add text entries to an XML file. What could be easier?
About the Author
Paul D. Sheriff is the president of PDSA Inc., a Microsoft Certified Partner in Southern California. Paul acts as the Microsoft regional director for Southern California, assisting the local Microsoft offices with Developer Days and several of the company’s large events each year. Paul has authored several books, webcasts, and articles on .NET, SQL Server, and SharePoint. You can reach Paul at PSheriff@pdsa.com or at Paul Sheriff’s Inner Circle.