ContentsOverview Although the term "delayed event handling" (DEH; also known as deferred, postponed or asynchronous event handling) might sound a bit arcane, I'd bet that you're quite familiar with the phenomena it represents. For example, the Windows Explorer implements DEH in its Folders pane: if you change the selected folder by using the keyboard arrow keys, the content (right) pane doesn't refresh immediately, but only after a small delay. This feature allows you to change the selected folder in a quick succession without the need to wait for refreshing the content pane after each of the folders in the sequence is selected. For example, imagine that you've the selected the Microsoft Enterprise Library folder and you want to use the down arrow key to select the Microsoft.NET folder as the following picture illustrates:
Imagine also that the enumeration of the folder content in the right pane takes 200 milliseconds in average and that the user can type as fast as the system is able to respond. Without DEH implementation, the process of selecting the Microsoft.NET folder starting from the Microsoft Enterprise Library folder would take 11 folders * 200 ms = 2.2 seconds. With DEH, however, the process would take about 200 milliseconds. That is a huge difference indeed. The more time it takes to refresh the right pane, the bigger the difference in favor of the DEH variant. If one utilizes the keyboard selection frequently in his work, one might find oneself wasting precious amounts of time waiting for unnecessary and unwanted content refreshes for folders not intended to be explored.
Incidentally, Windows Explorer is not the only well-known application supporting DEH; there are many others, for example Microsoft Outlook, Microsoft Outlook Express, Microsoft Management Console (MMC) and the various MMC snap-ins (Microsoft SQL Server, Event Viewer, Component Services, etc.).
In fact, many smart client UI designs for database-oriented applications often require refreshing portions of their windows in response to user selection. Because these refreshes often mean calling remote servers or web services, they're usually very time-consuming. Implementing DEH in such scenarios can significantly improve user experience, especially for keyboard-oriented users.
After implementing delayed event handling in several WinForms projects, I've realized that the approach I've used can be quite easily generalized and reused across many different controls and application scenarios. The remainder of this article describes my reusable DEH implementation and it is demonstrated by a demo solution accompanying the article. The demo solution has been developed in Visual Studio .NET 2003 and it resides in the deh.zip archive. After unzipping the archive (using the "Use Folder Names" WinZip option) the following structure is created on your drive:
| Individual.xml |
Data file used by the solution. |
| VB |
Subdirectory containing Visual Basic .NET version of the code. |
| |
Controls |
Subdirectory containing the TreeViewEx control implementation discussed in this article. |
| |
EventsLib |
Subdirectory containing the the DelayedEventDispatcher class discussed in this article. |
| CS |
Subdirectory containing the C# version of the code. It has the same underlying structure as the VB subdirectory. |
Top  Delayed event handling usage The DEH functionality is encapsulated in the DelayedEventDispatcher class (in the LaMarvin.Windows.Forms.Events assembly). To reuse the DEH functionality in your projects, just include the DelayedEventDispatcher.vb (VB.NET), or DelayedEventDispatcher.cs (C#) file into your project. (Alternatively, you can also reference the supplied LaMarvin.Windows.Forms.Events assembly in your project to reuse the class in binary form.)
The DelayedEventDispatcher class has been designed as a mediator between the source of the to-be-delayed event and the actual event handling code. It works on the following premises:
- The
DelayedEventDispatcher class doesn't itself capture the event to be delayed; it is the responsibility of the client to inform a DelayedEventDispatcher instance about the "authentic" event occurrence by calling the DelayedEventDispatcher.RegisterAuthenticEvent method.
- The
DelayedEventDispatcher class doesn't itself raise the delayed event. Instead, it invokes a delegate (passed to its constructor) whose responsibility is to actually handle the event. This design allows for two possible implementation approaches:
- One-off implementation.
- Encapsulated implementation.
The one-off approach is somewhat simpler as it doesn't involve any changes to the control that is the source of the to-be-delayed event. The approach is demonstrated in the TreeViewOneOffForm class implementation that is part of the accompanying demo application. The TreeViewOneOffForm consists of two panes - the left pane is a pre-populated TreeView control (ctlFolders) and the right pane displays the content of the selected TreeView node in a ListView control (ctlItems):
The content of the selected tree node is obtained by a call to a "fake" application server represented by an AppServer class. The AppServer class simulates a remote server call and it also allows the caller to specify how long the call should take (of course, this is useful for demonstration purposes only). Here is the code that handles the AfterSelect event (the error handling code is omitted for clarity): Private Sub ctlFolders_AfterSelect( _
ByVal sender As Object, ByVal e As TreeViewEventArgs)
' Clear the list view and force repainting in order to have the listview
' blank for the duration of the server call.
Me.ctlItems.Items.Clear()
Me.ctlItems.Refresh()
' Call the "server" specifying how long the call should last.
Dim items() As String = _
AppServer.GetFolderItems(e.Node.FullPath, Me.CallDuration)
' Populate the listview.
Dim item As String
For Each item In items
Me.ctlItems.Items.Add(item).ImageIndex = 2
Next
End Sub
The list view is cleared, then the server is called passing it the FullPath property of the selected node and the duration the server call should take. The server returns an array of strings representing "items" contained in the selected folder. Finally, the array of items is used to populate the list view. The following diagram shows the schematic structure of the code:
Now suppose that we want to postpone the TreeView.AfterSelect event handling to accommodate for fast keyboard-driven navigation. Using the DelayedEventDispatcher class, the structure of the code changes as follows:
- We've repackaged the event handling code into a separate method (
HandleAfterSelect) that has the same signature as the original ctlFolders_AfterSelect event handler.
- We've created an instance of the
DelayedEventDispatcher class (_afterSelectDispatcher) passing it a delegate pointing to the HandleAfterSelect method.
- In the original event handler method (
ctlFolders_AfterSelect), we've forwarded the event to the dispatcher by calling the _afterSelectDispatcher.RegisterAuthenticEvent method. That's all there is to it. Now when the user selects a folder, the ctlFolders tree view raises the AfterSelect event. The event is handled by the Form's ctlFolders_AfterSelect method, which simply passes the event arguments to the DelayedEventDispatcher instance (_afterSelectDispatcher.RegisterAuthenticEvent) and then immediately returns, allowing the user to interact with the form again. The DelayedEventDispatcher instance waits for a set amount of time (specified by the DelayedEventDispatcher.Delay property). If there is no another AfterSelect event registered within the delay interval, DelayedEventDispatcher executes the event handling code by invoking the HandleAfterSelect method (using the delegate passed to its constructor). The HandleAfterSelect method does the actual processing - populating the right-hand list view according to the currently selected folder.
Another example of the one-off DEH technique is demonstrated by the SearchIndividualForm class that can be found in the accompanying solution:
The user can type a string in the Search individual for: text box and the form calls the AppServer.SearchIndividual method that searches for a record matching the string in the supplied Individuals.xml file. The search is initiated in the TextChanged event handler that is delayed using also the one-off technique just described. For details, please see the SearchIndividualForm.vb or SearchIndividualForm.cs code files in the accompanying solution.
The advantage of the one-off approach is that it can be implemented rather easily. The disadvantage is that the implementation isn't reusable. For example, if there are several instances of a TreeView control and we'd like to delay AfterSelect processing for all of them, we'd have to declare, initialize and call a separate DelayedEventDispatcher instance for each of the TreeView instances used. In order to achieve reusability, we can use the second DEH implementation approach - the encapsulated implementation.
With encapsulated DEH implementation, we'll derive a new class from the TreeView control. The class will use an instance of the DelayedEventDispatcher class to implement delayed event handling internally. The client will handle the AfterSelect event just as any other "ordinary" event; it doesn't know that the event is actually delayed. The TreeViewEx class in the accompanying solution illustrates this approach:
First, the TreeViewEx class declares a private DelayedEventDispatcher member field and initializes it in the constructor: Public Class TreeViewEx
Inherits TreeView
Public Sub New()
Me._afterSelectDispatcher = New DelayedEventDispatcher( _
New TreeViewEventHandler(AddressOf Me.RaiseAfterSelect))
End Sub
Private _afterSelectDispatcher As DelayedEventDispatcher
...
Next, the TreeViewEx class overrides the OnAfterSelect protected method forwarding the event to the DelayedEventDispatcher instance: Protected Overrides Sub OnAfterSelect(ByVal e As TreeViewEventArgs)
Me._afterSelectDispatcher.RegisterAuthenticEvent(Me, e)
End Sub
It is very important to note that the OnAfterSelect override doesn't actually call the base class implementation, because that would raise the AfterSelect event to the client. Instead, a new private method with the appropriate signature is created: Private Sub RaiseAfterSelect( _
ByVal sender As Object, _
ByVal e As TreeViewEventArgs)
MyBase.OnAfterSelect(e)
End Sub
The RaiseAfterSelect method calls the base class' OnAfterSelect implementation actually raising the (already delayed) event to the client. The client doesn't have to do anything special, yet the AfterSelect event processing is delayed. The usage of the TreeViewEx class is demonstrated in the MainForm form class in the accompanying solution. When looking at the code, please note that the form uses the TreeViewEx instance exactly as it would use a TreeView instance.
In fact, if you'd like to implement DEH in your existing forms that use the standard TreeView control, you'd just replace references to the System.Windows.Forms.TreeView control with references to the TreeViewEx control and you'd have been done. No other change is necessary. Moreover, you can use the same approach with any other control exposing events that you want to have delayed.
Top  Customizing the delayed event handling behavior The DelayedEventDispatcher provides several methods for customizing the delayed event handling behavior: Public Property Delay() As TimeSpan
This property allows you to set the (approximate) interval for which an event is delayed. The delay can be specified in one of the DelayedEventDispatcher constructor overloads, and it can be also freely changed during the lifetime of the DelayedEventDispatcher instance. In my experience, the delay of about 350 milliseconds works best. (Although it's hard to measure exactly, it looks like a similar delay value is used by Microsoft Outlook - the role model for rich-client user interfaces.) Public Property DelayEnabled() As Boolean
Setting this property to False disables the delayed event handling mechanism altogether. If set to False, the RegisterAuthenticEvent method immediately (and synchronously) raises the event without any delay. The property is intended primarily for demonstration purposes - to show the difference between ordinary and delayed event handling. Public Property DelayMouseEvents() As Boolean
This property specifies whether events caused by mouse input should be delayed. In a normal course of things, processing of mouse events should occur without any delay, because the user is able to select an object directly (once again, you might want to verify the behavior with Outlook). The default value of this property is False.
Top  DelayedEventDispatcher implementation The implementation of the DelayedEventDispatcher class is rather simple. It uses an instance of the System.Windows.Forms.Timer class to periodically poll for (and eventually handle) previously registered events.
The state of an instance of the class is embodied in the following member fields: ' This points to a method that handles (processes) the event.
Private _processEventDelegate As System.Delegate
' Recent event data.
Private _recentEventArgs As EventArgs
Private _recentSender As Object
Private _recentEventTime As DateTime
' Polling timer.
Private WithEvents _timer As Timer
' How long to delay the event.
Private _delay As TimeSpan
The _processEventDelegate delegate reference is used for invoking event processing for the delayed event. Clients pass a reference to an event-handling delegate to the DelayedEventDispatcher constructor: Public Sub New( _
ByVal processEventDelegate As System.Delegate)
<validation code omitted for clarity>
Me._processEventDelegate = processEventDelegate
By using the System.Delegate type for the _processEventDelegate member, we've achieved great flexibility in what type of delegates we can pass to the constructor. However, the DelayedEventDispatcher class expects the delegate to be compatible with the System.EventHandler delegate. That is, the referenced method has to have two arguments: the first one being compatible with System.Object, the second one being compatible with System.EventArgs class, respectively: Public Delegate Sub EventHandler( _
ByVal sender As Object, _
ByVal e As EventArgs)
The delegate is validated in the DelayedEventDispatcher constructor at runtime and an ArgumentException is thrown if the delegate type is not compatible.
The _recent* members hold the event arguments and the time of the event recently registered (by means of the client calling the RegisterAuthenticEvent method shown later).
Finally, the _timer instance is used to check periodically to see if an event has recently been registered and the required delay interval has already passed: Private Sub _timer_Tick(ByVal sender As Object, ByVal e As EventArgs)
Handles _timer.Tick
If (Me._recentEventArgs Is Nothing) Then
Return
End If
Dim elapsed As TimeSpan = DateTime.op_Subtraction(DateTime.Now,
Me._recentEventTime)
If TimeSpan.op_LessThan(elapsed, Me._delay) Then
Return
End If
Me.HandleEvent()
End Sub
First, there is a check if any event has been recently registered. If not, there is nothing to do and the code bails out. If there was an event registered recently, the code checks to see if the event is "older" that the required delay interval. If it is, the event processing is initiated by calling the HandleEvent private method: Private Sub HandleEvent()
Debug.Assert(Not Me._recentEventArgs Is Nothing)
Try
' Prepare the argument array and discard the recent event args BEFORE
' invoking the event delegate. Otherwise reentrant calls could occur if the
' event handling code yields (for instance calls Application.DoEvents).
' Thanks to Matt Stone for pointing this out to me!
Dim args() As Object = New Object() {Me._recentSender, Me._recentEventArgs}
Me._recentEventArgs = Nothing
Me._recentSender = Nothing
' Now call the event delegate.
Me._processEventDelegate.DynamicInvoke(args)
Catch ex As Exception
Trace.WriteLine(ex.ToString())
End Try
End Sub
The HandleEvent method clears the recent event data (so the event is not processed again by the next timer tick) and then it actually invokes the _processEventDelegate delegate. The Delegate.DynamicInvoke method is used passing it the original event arguments in an Object-based array.
Clients register events for delayed processing by calling the RegisterAuthenticEvent method: Public Sub RegisterAuthenticEvent( _
ByVal sender As Object, _
ByVal args As EventArgs)
' Save the event state.
Me._recentEventArgs = args
Me._recentSender = sender
Me._recentEventTime = DateTime.Now
' If postponing is disabled, raise the event directly.
If (Not Me.DelayEnabled) Then
Me.HandleEvent()
Return
End If
' If the event was caused by a mouse, raise the event "almost"
' immediately (by pretending the event occurred at least this._delay
' milliseconds in the past).
' Please note: I'm saying almost because we're still using the timer to
' raise the event on the next Tick (at most 20 milliseconds from now).
' This ensures that a TreeView, for example, will display the new node in
' a selected state before the AfterClick event is raised.
If (Not Me._delayMouseEvents AndAlso _
(GetAsyncKeyState(Me._vkLeftButton) <> 0)) Then
Me._recentEventTime = DateTime.op_Subtraction( _
DateTime.op_Subtraction(DateTime.Now, Me._delay), _
TimeSpan.FromMilliseconds(100))
End If
End Sub
The public RegisterAuthenticEvent stores the passed-in event arguments in member variables and records the time it was called. If the DelayEnabled property is set to False (meaning the client doesn't want to delay events), the HandleEvent method is called immediately causing the event to be processed synchronously. If delaying the event is feasible, the code then checks to see if the event wasn't caused by mouse input (by using the GetAsyncKeyState API). If the event was indeed caused by the mouse (and delaying mouse events is not disabled), the event is scheduled to be handled by the next timer poll. Because the poll interval is set to 20 milliseconds (this is hard-coded in the DelayedEventDispatcher constructor), this event will be processed after at most 20 milliseconds apart from its occurrence.
That's all there is to it. For details, please check the source code accompanying the article.Top 
About
Palo Mraz
Click here if you want to know more about
Palo Mraz.
Other articles that may interest you
|