|
An ordinary VB developer shares his own successes and failures |
|
|||||||||||||||
| Home News Articles Resources Tips Downloads About me | |||||||||||||||||
Delayed event handling for WinForms applicationsUse delayed event handling to improve responsiveness of your WinForms user interfaces.Download the accompanying VS.NET solution.Discuss here. OverviewAlthough 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:
Delayed event handling usageThe DEH functionality is encapsulated in theDelayedEventDispatcher 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:
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:
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 SubIt 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 SubThe 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.
Customizing the delayed event handling behaviorTheDelayedEventDispatcher provides several methods for customizing the
delayed event handling behavior:
Public Property Delay() As TimeSpanThis 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 BooleanSetting 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 BooleanThis 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.
DelayedEventDispatcher implementationThe implementation of theDelayedEventDispatcher 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 TimeSpanThe _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 = processEventDelegateBy 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. Discuss the article here. © Palo Mraz, Wednesday, June 08, 2005 |
|||||||||||||||||
|
|
|||||||||||||||||
| ©2003-2008 Palo Mraz. All Rights Reserved. See my 'new browser window' policy | |||||||||||||||||