In my last blog post, I talked about a filter enhancement to a ComboBox that could be added to a single combo box with just one line of code. The blog post wasn’t necessarily to talk about the code, but was to talk about a good architecture and the importance of a good architecture for a system or application.

This post will focus on the actual class that was created in order to allow us to have this functionality.
The code was designed for WinForms Infragistics UltraCombo controls (v6.2 CLR2.0). These controls are multi column drop down lists, essentially. We’re using this control and not the native WinForms ComboBox because of the multi column ability. In this organization, we use a lot of Infragistics controls, but there are other vendors who make very good controls as well.

Before (without filter functionality):

After (with new filter functionality enabled):

(Some information in screen shots above is blurred out to protect customer confidentiality)

As the user is typing into the UltraCombo control the list filters based on what they are typing. In the example above the user has typed in ‘007’, so the list shows any items that have ‘007’ somewhere in the value. As the user is typing in order to filter, a filter icon is displayed in the UltraCombo. All of this functionality is encapsulated in the UltraComboTypeFilter class.

Here is the code for the UltraComboTypeFilter class that will give any UltraCombo control this functionality:

''' <summary>
''' Class is used to encapsulate functionality to allow user to type into
''' ultra combo drop down and have the list display and filter the list as
''' the user types
''' Dan Douglas Mar 5, 2010 https://dandouglas.wordpress.com
''' </summary>
''' <remarks></remarks>
Public Class UltraComboTypeFilter
Dim UltraComboControl As Infragistics.Win.UltraWinGrid.UltraCombo
Dim KeyColumn As String
Dim _FilterImage As Image
''' <summary>
''' Create a new instance of the UltraComboTypeFilter class
''' </summary>
''' <param name="UltraCombo">The Infragistics UltraComboBox control to apply the filter functionality to</param>
''' <param name="ColumnToFilter">The key of the column you want to be searched for filtering</param>
''' <remarks></remarks>
Public Sub New(ByVal UltraCombo As Infragistics.Win.UltraWinGrid.UltraCombo, ByVal ColumnToFilter As String)
UltraComboControl = UltraCombo
'Add handlers so that the methods in this class can handle the events from the control
AddHandler UltraComboControl.KeyUp, AddressOf ucbo_KeyUp
AddHandler UltraComboControl.AfterCloseUp, AddressOf ucbo_AfterCloseUp
AddHandler UltraComboControl.TextChanged, AddressOf ucbo_TextChanged
AddHandler UltraComboControl.BeforeDropDown, AddressOf ucbo_BeforeDropDown
KeyColumn = ColumnToFilter
FilterImage = My.Resources.FilterIcon() 'the filter icon is storred as an embedded resource in the resource file
'turn off automatic value completion as it can potentially interfere at times with the search/filter functionality
UltraComboControl.AutoEdit = False
HideFilterIcon()
UltraComboControl.Appearance.ImageHAlign = Infragistics.Win.HAlign.Right 'filter icon will be always displayed on the right side of the text area of the control
ClearCustomPartFilter() 'by default, clear filters
End Sub
Private Sub ShowFilterIcon()
'add the filter icon to the ComboBox
UltraComboControl.Appearance.Image = FilterImage
End Sub
Private Sub HideFilterIcon()
UltraComboControl.Appearance.Image = Nothing
End Sub
Private Sub ucbo_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs)
If Trim(UltraComboControl.Text) = "" Then
ClearCustomPartFilter() 'if there are no characters in the textbox (from dropdown) then remove filters
End If
End Sub
Private Sub ClearCustomPartFilter()
'clear any filters if they exist
UltraComboControl.DisplayLayout.Bands(0).ColumnFilters.ClearAllFilters()
HideFilterIcon()
End Sub
Private Sub DoPartDropDownFilter()
UltraComboControl.DisplayLayout.Bands(0).ColumnFilters.ClearAllFilters()
UltraComboControl.DisplayLayout.Bands(0).ColumnFilters(KeyColumn).FilterConditions.Add(Infragistics.Win.UltraWinGrid.FilterComparisionOperator.Like, "*" & UltraComboControl.Text & "*")
ShowFilterIcon()
End Sub
Private Sub ucbo_BeforeDropDown(ByVal sender As Object, ByVal e As System.ComponentModel.CancelEventArgs)
'clear any filters if they exist before the user drops down the list, if the user starts typing again - filter will be shown
'this is done so that if the user leaves the combo box and then goes back to it and drops down the list the full list will be
'there until they start typing a filter again; this is by design
ClearCustomPartFilter()
End Sub
Private Sub ucbo_KeyUp(ByVal sender As Object, ByVal e As System.Windows.Forms.KeyEventArgs)
'the code in this method is to start the filtering process to filter the drop down list if the drop down isn't 'dropped'
'with thi procedure the user can just start typing into the combo box and have the box drop down automatically and filter
'KeyPress event is not used because of timing issues - the timing of the event is too late for us to filter properly
'Do not do filter or drop down if user hits ESC - also we will check other non entry keys like Left, Right, etc)
''Array of keys that we won't do anything with
Dim IgnoreKeys As New List(Of Integer)
IgnoreKeys.Add(Keys.Left)
IgnoreKeys.Add(Keys.Right)
IgnoreKeys.Add(Keys.Up)
IgnoreKeys.Add(Keys.Down)
IgnoreKeys.Add(Keys.Escape)
IgnoreKeys.Add(Keys.Enter)
If IgnoreKeys.Contains(e.KeyCode) = False Then
'if inputted key press is valid for drop down filtering
Dim iSelLoc As Integer = UltraComboControl.Textbox.SelectionStart 'get location of cursor
If UltraComboControl.IsDroppedDown = False Then
UltraComboControl.ToggleDropdown()
'toggling drop down causes all text to be highlighted so we will deselect it and put the cursor position back where it was instead of being at 0
UltraComboControl.Textbox.SelectionLength = 0
UltraComboControl.Textbox.SelectionStart = iSelLoc
End If
DoPartDropDownFilter()
End If
End Sub
''' <summary>
''' The image to use for the filter icon shown on the control to be displayed while the control is filtered
''' </summary>
''' <value></value>
''' <returns></returns>
''' <remarks></remarks>
Public Property FilterImage() As Image
Get
Return _FilterImage
End Get
Set(ByVal value As Image)
_FilterImage = value
End Set
End Property
Private Sub ucbo_AfterCloseUp(ByVal sender As Object, ByVal e As System.EventArgs)
ClearCustomPartFilter()
End Sub
Protected Overrides Sub Finalize()
RemoveHandler UltraComboControl.KeyUp, AddressOf ucbo_KeyUp
RemoveHandler UltraComboControl.AfterCloseUp, AddressOf ucbo_AfterCloseUp
RemoveHandler UltraComboControl.TextChanged, AddressOf ucbo_TextChanged
RemoveHandler UltraComboControl.BeforeDropDown, AddressOf ucbo_BeforeDropDown
MyBase.Finalize()
End Sub
End Class

Now, to enable the functionality on any UltraCombo control just use one single line of code.

Dim PartFilterFunction As New UltraComboTypeFilter(ucboPart, "CustPartNo")

Add the filter icon/image as a resource to the project that contains the UltraComboTypeFilter class and name the resource FilterIcon.

The IEditableObject interface in .NET is part of the System.Component model namespace and is used to provide functionality to your objects that implement this interface that allows the object to commit or roll back changes. Typically your business objects will implement this interface in order for them to natively have the commit and rollback functionality you desire. When using the business object as a data source that is bound to a control such as a grid control, the grid control recognizes the IEditableObject implementation and interfaces with it directly in order to achieve the desired commit/rollback functionality desired without having to write any other custom code in your UI layer or for the grid component.

We make heavy use of 3rd party components within our applications, and a big one that we use is the Infragistics WinGrid. This provides excellent capability and works great with .NET datasets. It also integrates well with business objects or lists of business objects as a data source, but unless your business object implements IEditableObject you will not get the native commit/rollback functionality you would get with a dataset. For example – the user starts changing a record on a grid in a windows form and hits the Esc key twice to cancel their changes – this will not actually rollback these changes to the business object unless the business object contains logic to do so.

Like a lot of things when it comes to software development there are a few ways to accomplish this 1) A quick and dirty way that might work for the current business object, but requires a lot of customization for each additional business object that you want to introduce the functionality to 2) A thought out where we are really thinking about the architecture and that allows you to write the code once and re-use that same code where needed

Ok, so we have a base class that we created and we called it BusinessObjectBase and it implements IEditableObject. The three methods of IEditableObject are pretty self describing – BeginEdit, CancelEdit, and EndEdit.

Here’s how we do it:

1) Created a MustInherit Class (Abstract in C#) – This is the class that all business object that we want to incorporate this functionality into will Inherit

2) Implement IEditableObject

3) Write code for each of the three methods that will give us this desired functionality.

BeginEdit – Copies the current object (itself) into a new object of the same type. This uses the MemberwiseClose method in order to create a copy of the current object to be used as the object we will revert to if the changes are cancelled. This allows us to reuse this functionality as is without having to customize it for other object that have different property definitions. We also set an internal boolean variable used as a flag to signify ‘edit mode’ to true.

CancelEdit – Uses reflection to copy the property values from the copied object that was created in the BeginEdit method. It cycles through all of the public properties using reflection and copies those values back to the current object overwriting any changes made during the edit. The MemberwiseClone method in this case could not be used because an object instance cannot set itself to reference another object from within itself. Additional functionality was added to CancelEdit so that it would remove itself from the parent list that it was in if it was a new object. This was put in place because grid components automatically add new objects to a list when adding a new object to the grid, so in this case if the edit is cancelled then we want to destroy the object as it never existed in the first place. Please see related blog post Business Objects That Support a Parent/Child Relationship in .NET for information on how we implement the Parent/Child functionality that allows the child to reference its parent and provide the functionality needed here to remove new items from the parent list when they are cancelled.

EndEdit – Editing is complete and the changes are finalized, so we set the old version of the object that we created to backup the original data to nothing. We also set the internal edit mode flag to false.

4) Create a new business object class (or use an existing one) and inherit BusinessObjectBase – you will now be able to use this class object in user interface components and retain commit/rollback functionality.

<serializable ()> _
Public MustInherit Class BusinessObjectBase
Implements ComponentModel.IEditableObject
Protected m_oOldVersion As BusinessObjectBase
Dim m_bInEditMode As Boolean
''' <summary>
''' The IList collection object that this object is part of. If this object is not a child object in an IList object then Nothing is returned.
''' </summary>
'<nonserialized ()> _
Public ParentList As System.Collections.IList
''' <summary>
''' The parent object. If this is a top level object then Nothing is returned.
''' </summary>
Public Parent As BusinessObjectBase ' BusinessObjectBase
''' <summary>
''' Is the current instance of the business object a new instance that has not been committed to the database.
''' This is usually implemented by checking the value of an identifier property that would relate to a primary key in the database
''' </summary>
Public MustOverride Function IsNew() As Boolean
''' <summary>
''' Method invoked by databound controls to signal that the object is going into edit mode (begin editing)
''' </summary>
''' <remarks></remarks>
Public Sub BeginEdit() Implements System.ComponentModel.IEditableObject.BeginEdit
'store the old version of this object - so if we need to undo we will revert back to it
If m_bInEditMode = False Then
m_bInEditMode = True
If m_oOldVersion Is Nothing Then 'this is only nothing when there are not pending changes to the object
m_oOldVersion = Me.MemberwiseClone()
End If
End If
End Sub
''' <summary>
''' Method invoked by databound controls to signal a cancel/rollback of the changes that have been made
''' to the object since editing began. If editing an existing object then changes are rolled back.
''' If editing a new object this new instance is removed from it's parent list.
''' </summary>
''' <remarks></remarks>
Public Sub CancelEdit() Implements System.ComponentModel.IEditableObject.CancelEdit
Dim o As BusinessObjectBase
o = Me
If o.IsNew Then
'this is a new object that hasn't been committed to the database
o.ParentList.Remove(Me)
Else
'revert back to previous
RevertObject()
End If
m_bInEditMode = False
End Sub
''' <summary>
''' The property values of the current instance of this object are
''' reverted back to their original values as of the BeginEdit method call.
''' Reflection is used to revert from the old version of the class to
''' this instance by cycling through each property and setting the value
''' of this instances property equal to the value of the old version of the objects
''' property value.
''' </summary>
''' <remarks></remarks>
Private Sub RevertObject()
Dim t As Type = Me.GetType
Dim p As Reflection.PropertyInfo
For Each p In t.GetProperties()
If p.CanWrite And p.CanRead Then
'get original property value
Dim oValue As Object = Nothing
oValue = p.GetValue(m_oOldVersion, Nothing)
'set value of this instance equal to original value
p.SetValue(Me, oValue, Nothing)
End If
'Next
Next
End Sub
''' <summary>
''' Editing of this object has been completed
''' </summary>
''' <remarks></remarks>
Public Sub EndEdit() Implements System.ComponentModel.IEditableObject.EndEdit
m_bInEditMode = False
m_oOldVersion = Nothing
End Sub
End Class

Follow Blog via Email

Dan Douglas is based in Toronto, Ontario, Canada and does consulting work for both small organizations and large global organizations through his consulting company, Douglas Information Systems Corporation. He is an experienced and proven subject matter expert, decision maker, and leader in the area of Software Development and Architecture.

With over 16 years of experience, Dan has been the Architect Lead on over 15 development projects and has successfully delivered large scale “best in class” end to end solutions. Dan has developed and architected solutions across a wide vertical, including, government, medical, automative, hr, manufacturing, technology, consulting, and software firms.
Dan writes a lot of code as a hands on developer and is passionate about delivering the right solutions to customers through better code, better architected solutions, better business alignment, and better process.

"My articles are inspired by what's possible. My experience in my software consulting practice has given me the inspiration to write about what I've seen and what I've done, and to write about 'What's possible in software'." - Dan Douglas