|
||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionGoal: Demonstrate how to keep WPF UIs responsive while running long processes asynchronously. Method(s): Through a sample application, I will first demonstrate what a non-responsive UI is and how you get one. Next, I will demonstrate how to make the UI responsive through asynchronous code. Finally, I will demonstrate how to make the UI responsive through a much simpler, event-based, asynchronous approach. I will also show how to keep the user informed while the processing takes place, with updates to What is a non-responsive UI? Surely, we’ve all witnessed a Windows Form or WPF application that “locks up” from time to time. Have you ever thought of why this happens? In a nutshell, it’s typically because the application is running on a single thread. Whether it’s updating the UI or running some long process on the back end, such as a call to the database, everything must get into a single file line and wait for the CPU to execute the command. So, when we are making that call to the database that takes a couple seconds to run, the UI is left standing in line waiting, unable to update itself, and thus “locking up”. How can this unresponsive UI problem be resolved? Whether it’s a Windows Form or WPF application, the UI updates on the main or primary thread. In order to keep this thread free so the UI can remain responsive, we need to create a new thread to run any large tasks on the back-end. The classes used to accomplish this have evolved over the different releases of the .NET Framework, becoming easier, and richer in capabilities. This, however, can cause some confusion. If you do a simple Google search on C# or VB and asynchronous, or something similar, you are sure to get results showing many different ways of accomplishing asynchronous processing. The answer to the question, “which one do I use?” of course depends on what you’re doing and what your goals are. Yes, I hate that answer also. Since I cannot possibly cover every asynchronous scenario, what I would like to focus on in this article is what I have found myself needing asynchronous processing for majority of the time. That would be keeping the UI of a WPF application responsive while running a query on the database. Please note that with some minor modifications, the code in this article and in the downloadable source code can be run for a Windows Form application also. In addition, this article shows how to solve a specific problem with asynchronous programming, by no means though is this the only problem asynchronous programming is used for. To help demonstrate synchronous, asynchronous, and event-driven asynchronous processing, I will work through an application that transgresses through several demos:
The code in this article will be written in VB; however, full source code download will be available in both C# and VB versions. What Not To Do
As I mentioned previously, what you do not want to do is run all your processing both back-end and UI on a single thread. This will almost always lead to a UI that locks up. You can download the demo application in both C# and VB versions. Run the application, and click the Start button under Synchronous Demo. As soon as you click the button, try to drag the window around your screen. You can’t. If you try it several times, the window may even turn black, and you will get a “(Not Responding)” warning in the title bar. However, after several seconds, the window will unlock, the UI will update, and you can once again drag it around your screen freely. Let’s look at this code to see what’s going on. If you look at the code for this demo, you will see the following: First, we have a delegate which is sort of like a function pointer, but with more functionality and providing type safety. Delegate Function SomeLongRunningMethodHandler(ByVal rowsToIterate As Integer) As String
We could easily not use the delegate in this sample, and simply call the long running method straight from the method handler. In fact, if I didn't already know I was going to change this call to run asynchronously, I wouldn't use a delegate. However, by using the delegate, I can demonstrate how easy it is to go from a synchronous call to an asynchronous call. In other words, let’s say you have a method that you may want to run asynchronously but you aren’t sure. By using a delegate, you can make the call synchronously now, and later switch to an asynchronous call with little effort. I’m not going to go into too much detail on delegates, but the key to remember is that the signature of the delegate must exactly match the signature of the function (or Next, we have the method handler for the Remember when I said that by using delegates we can easily move from synchronous to asynchronous? You now have a clue as to how, and we will get into the details of that soon. Going back to our asynchronous example, you can see the Private Sub SynchronousStart_Click(ByVal sender As System.Object, _
ByVal e As System.Windows.RoutedEventArgs) _
Handles synchronousStart.Click
Me.synchronousCount.Text = ""
Dim synchronousFunctionHandler As SomeLongRunningMethodHandler
synchronousFunctionHandler = _
New SomeLongRunningMethodHandler(AddressOf _
Me.SomeLongRunningSynchronousMethod)
Dim returnValue As String = _
synchronousFunctionHandler.Invoke(1000000000)
Me.synchronousCount.Text = _
"Processing completed."&
returnValue & " rows processed."
End Sub
This is the function that the delegate calls. As mentioned earlier, it could have also been called directly without the use of a delegate. It simply takes an integer, and iterates that many times, returning the count as a string when completed. This method is used to mimic any long running process you may have. Private Function SomeLongRunningSynchronousMethod _
ByVal rowsToIterate As Integer) As String
Dim cnt As Double = 0
For i As Long = 0 To rowsToIterate
cnt = cnt + 1
Next
Return cnt.ToString()
End Function
The bad news is that implementing this demo asynchronously causes an unresponsive UI. The good news is that by using a delegate, we have set ourselves up to easily move to an asynchronous approach and a responsive UI. A More Responsive ApproachNow, run the downloaded demo again, but this time, click the second Run button (Synchronous Demo). Then, try to drag the window around your screen. Notice anything different? You can now click the button which calls the long running method and drag the window around at the same time, without anything locking up. This is possible because the long running method is run on a secondary thread, freeing up the primary thread to handle all the UI requests.
This demo uses the same Delegate Function AsyncMethodHandler _
ByVal rowsToIterate As Integer) As String
Delegate Sub UpdateUIHandler _
ByVal rowsupdated As String)
Private Sub AsynchronousStart_Click( _
ByVal sender As System.Object, _
ByVal e As System.Windows.RoutedEventArgs)
Me.asynchronousCount.Text = ""
Me.visualIndicator.Text = "Processing, Please Wait...."
Me.visualIndicator.Visibility = Windows.Visibility.Visible
Dim caller As AsyncMethodHandler
caller = New AsyncMethodHandler _
(AsyncMethodHandlerAddressOf Me.SomeLongRunningSynchronousMethod)
caller.BeginInvoke(1000000000, AddressOf CallbackMethod, Nothing)
End Sub
Notice that the event method starts out similar to the previous example. We setup some UI controls, then we declare and instantiate the first delegate. After that, things get a little different. Notice the call from the delegate instance “ Essentially, there are multiple ways to handle asynchronous execution using
We will use the last technique, executing a callback method when the asynchronous call completes. We can use this method because the primary thread which initiates the asynchronous call does not need to process the results of that call. Essentially, what this enables us to do is call You can see that our call to Protected Sub CallbackMethod(ByVal ar As IAsyncResult)
Try
Dim result As AsyncResult = CType(ar, AsyncResult)
Dim caller As AsyncMethodHandler = CType(result.AsyncDelegate, _
AsyncMethodHandler)
Dim returnValue As String = caller.EndInvoke(ar)
UpdateUI(returnValue)
Catch ex As Exception
Dim exMessage As String
exMessage = "Error: " & ex.Message
UpdateUI(exMessage)
End Try
End Sub
In the callback method, the first thing we need to do is get a reference to the calling delegate (the one that called Once After Sub UpdateUI(ByVal rowsUpdated As String)
Dim uiHandler As New UpdateUIHandler(AddressOf UpdateUIIndicators)
Dim results As String = rowsUpdated
Me.Dispatcher.Invoke(Windows.Threading.DispatcherPriority.Normal, _
uiHandler, results)
End Sub
Sub UpdateUIIndicators(ByVal rowsupdated As String)
Me.visualIndicator.Text = "Processing Completed."
Me.asynchronousCount.Text = rowsupdated & " rows processed."
End Sub
Next, we can see the Next, you will see the call to We have now successfully written a responsive multi-threaded WPF application. We have done it using delegates, Asynchronous Event-Based Model
There are many approaches to writing asynchronous code. We have already looked at one such approach, which is very flexible should you need it. However, as of .NET 2.0, there is what I would consider a much simpler approach, and safer. The Consider the following method which we have decided to spin off on a separate thread so that the UI can remain responsive. Private Function SomeLongRunningMethodWPF() As String
Dim iteration As Integer = CInt(100000000 / 100)
Dim cnt As Double = 0
For i As Long = 0 To 100000000
cnt = cnt + 1
If (i Mod iteration = 0) And (backgroundWorker IsNot Nothing) _
AndAlso backgroundWorker.WorkerReportsProgress Then
backgroundWorker.ReportProgress(i \ iteration)
End If
Next
Return cnt.ToString()
End Function
Notice, there is also some code to keep track of the progress. We will address this as we get to it; for now, just keep in mind we are reporting progress to the Using the
I will quickly demonstrate the latter method, but for the remainder of the demo, we will use the declarative approach. First, you must reference the namespace for <Window x:Class="AsynchronousDemo"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cm="clr-namespace:System.ComponentModel;assembly=System"
Title="Asynchronous Demo" Height="400" Width="450">
Then, you can create an instance of the <Window.Resources>
<cm:BackgroundWorker x:Key="backgroundWorker" _
WorkerReportsProgress="True" _
WorkerSupportsCancellation="False" />
</Window.Resources>
Declaratively, we could accomplish the same thing: Private WithEvents backgroundWorker As New BackgroundWorker()
Next, we need something to call the long running process to kick things off. In our demo, we will trigger things with the Private Sub WPFAsynchronousStart_Click(ByVal sender As System.Object, _
ByVal e As System.Windows.RoutedEventArgs)
Me.wpfCount.Text = ""
Me.wpfAsynchronousStart.IsEnabled = False
backgroundWorker.RunWorkerAsync()
wpfProgressBarAndText.Visibility = Windows.Visibility.Visible
End Sub
Let’s go through what’s happening in the button click event. First, we clear out any text that’s in our When the button is clicked, we are also capturing that event in a Storyboard located in the XAML. This Storyboard triggers the animation directed at a <StackPanel.Triggers>
<EventTrigger RoutedEvent="Button.Click"
SourceName="wpfAsynchronousStart">
<BeginStoryboard Name="myBeginStoryboard">
<Storyboard Name="myStoryboard"
TargetName="wpfProgressBar"
TargetProperty="Value">
<DoubleAnimation
From="0"
To="100"
Duration="0:0:2"
RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</StackPanel.Triggers>
Private Sub backgroundWorker_DoWork(ByVal sender As Object, _
ByVal e As DoWorkEventArgs) _
Handles backgroundWorker.DoWork
Dim result As String
result = Me.SomeLongRunningMethodWPF()
e.Result = result
End Sub
There are a few important things to note about Remember, in our long running process, I noted that we were tracking progress? Specifically, every 100 iterations of the loop, we were calling: backgroundWorker.ReportProgress(i \ iteration)
The method Private Sub backgroundWorker_ProgressChanged(ByVal sender As Object, _
ByVal e As System.ComponentModel.ProgressChangedEventArgs) _
Handles backgroundWorker.ProgressChanged
Me.wpfCount.Text = _
CStr(e.ProgressPercentage) & "% processed."
End Sub
We are using this method to update a Private Sub backgroundWorker_RunWorkerCompleted(ByVal sender As Object, _
ByVal e As RunWorkerCompletedEventArgs) _
Handles backgroundWorker.RunWorkerCompleted
wpfProgressBarAndText.Visibility = Windows.Visibility.Collapsed
Me.wpfCount.Text = "Processing completed. " & _
CStr(e.Result) & " rows processed."
Me.myStoryboard.Stop(Me.lastStackPanel)
Me.wpfAsynchronousStart.IsEnabled = True
End Sub
In the The downloadable code which is available in both C# and VB also contains code which handles the
|
|||||||||||||||||||||||||||||||||||||||||||||||