Designing and Developing Windows Applications Using Microsoft .NET Framework 4: Designing the Presentation Layer
- 12/17/2011
- Objective 2.1: Choose the Appropriate Windows Technology
- Objective 2.2: Design the UI Layout and Structure
- Objective 2.3: Design Application Workflow
- Objective 2.4: Design Data Presentation and Input
- Objective 2.5: Design Presentation Behavior
- Objective 2.6: Design for UI Responsiveness
- Objective 2.6: Design for UI Responsiveness
- Chapter Summary
- Answers
Objective 2.6: Design for UI Responsiveness
While the primary function of the Presentation layer is to interact with the user, you also can use the computer power of the client by handling computing tasks that do not require communication with the server or data layers. Using multithreaded techniques enables you to use the processing power of the client while maintaining responsiveness in the UI.
Offloading Operations from the UI Thread and Reporting Progress
The BackgroundWorker component is designed to allow you to execute time-consuming operations on a separate, dedicated thread. This allows you to run operations that take a lot of time, such as file downloads and database transactions asynchronously and allow the UI to remain responsive.
The key method of the BackgroundWorker component is the RunWorkerAsync method. When this method is called, the BackgroundWorker component raises the DoWork event. The code in the DoWork event handler is executed on a separate, dedicated thread, allowing the UI to remain responsive.
Announcing the Completion of a Background Process
When the background process terminates, whether because the process is completed or because the process is cancelled, the RunWorkerCompleted event is raised. You can alert the user to the completion of a background process by handling the RunWorkerCompleted event.
Returning a Value from a Background Process
You might want to return a value from a background process. For example, if your process is a complex calculation, you would want to return the end result. You can return a value by setting the Result property of DoWorkEventArgs in DoWorkEventHandler. This value will then be available in the RunWorkerCompleted event handler as the Result property of the RunWorkerCompletedEventArgs parameter.
Cancelling a Background Process
You might want to implement the ability to cancel a background process. BackgroundWorker supports the ability to cancel a background process, but you must implement most of the cancellation code yourself. The WorkerSupportsCancellation property of the BackgroundWorker component indicates whether the component supports cancellation. You can call the CancelAsync method to attempt to cancel the operation; doing so sets the CancellationPending property of the BackgroundWorker component to True. By polling the CancellationPending property of the BackgroundWorker component, you can determine whether to cancel the operation.
Reporting Progress of a Background Process with BackgroundWorker
For particularly time-consuming operations, you might want to report progress back to the primary thread. You can report progress of the background process by calling the ReportProgress method. This method raises the BackgroundWorker.ProgressChanged event and allows you to pass a parameter that indicates the percentage of progress that has been completed to the methods that handle that event. The following example demonstrates how to call the ReportProgress method from within the BackgroundWorker.DoWork event handler and then to update a ProgressBar control in the BackgroundWorker.ProgressChanged event handler:
Sample of Visual Basic.NET Code
Private Sub BackgroundWorker1_DoWork(ByVal sender As System.Object, _ ByVal e As System.ComponentModel.DoWorkEventArgs) _ Handles BackgroundWorker1.DoWork For i As Integer = 1 to 10 RunTimeConsumingProcess() ' Calls the Report Progress method, indicating the percentage ' complete BackgroundWorker1.ReportProgress(i*10) Next End Sub Private Sub BackgroundWorker1_ProgressChanged( _ ByVal sender As System.Object, _ ByVal e As System.ComponentModel.ProgressChangedEventArgs) _ Handles BackgroundWorker1.ProgressChanged ProgressBar1.Value = e.ProgressPercentage End Sub
Sample of C# Code
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) { for (int i = 1;i < 11; i++) { RunTimeConsumingProcess(); // Calls the Report Progress method, indicating the percentage // complete backgroundWorker1.ReportProgress(i*10); } } private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) { progressBar1.Value = e.ProgressPercentage; }
Note that in order to report progress with the BackgroundWorker component, you must set the WorkerReportsProgress property to True.
Requesting the Status of a Background Process
You can determine if a BackgroundWorker component is executing a background process by reading the IsBusy property. The IsBusy property returns a Boolean value. If True, the BackgroundWorker component is currently running a background process. If False, the BackgroundWorker component is idle.
Creating Process Threads
For applications that require more precise control over multiple threads, you can create new threads with the Thread object. The Thread object represents a separate thread of execution that runs concurrently with other threads. You can create as many Thread objects as you like, but the more threads there are, the greater the impact on performance and the greater the possibility of adverse threading conditions, such as deadlocks.
Creating and Starting a New Thread
The Thread object requires a delegate to the method that will serve as the starting point for the thread. This method must be a Sub (void in C#) method and must take either no parameters or a single Object parameter. In the latter case, the Object parameter is used to pass any required parameters to the method that starts the thread. Once a thread is created, you can start it by calling the Thread.Start method. The following example demonstrates how to create and start a new thread:
Sample of Visual Basic.NET Code
Dim aThread As New System.Threading.Thread(Addressof aMethod) aThread.Start()
Sample of C# Code
System.Threading.Thread aThread = new System.Threading.Thread(aMethod); aThread.Start();
For threads that accept a parameter, the procedure is similar, except that the starting method can take a single Object as a parameter and that object must be specified as the parameter in the Thread.Start method. An example is shown below:
Sample of Visual Basic.NET Code
Dim aThread As New System.Threading.Thread(Addressof aMethod) aThread.Start(anObject)
Sample of C# Code
System.Threading.Thread aThread = new System.Threading.Thread(aMethod); aThread.Start(anObject);
Destroying Threads
You can destroy a Thread object by calling the Thread.Abort method. This method causes the thread on which it is called to cease its current operation and to raise a ThreadAbortException. If there is a Catch block that is capable of handling the exception, it will execute along with any Finally blocks. The thread then is destroyed and cannot be restarted.
Sample of Visual Basic.NET Code
aThread.Abort()
Sample of C# Code
aThread.Abort();
Synchronizing Threads
Two of the most common difficulties involved in multithread programming are deadlocks and race conditions. A deadlock occurs when one thread has exclusive access to a particular variable and then attempts to gain exclusive access to a second variable at the same time that a second thread has exclusive access to the second variable and attempts to gain exclusive access to the variable that is locked by the first thread. The result is that both threads wait indefinitely for the other to release the variables and they cease operating.
A race condition occurs when two threads attempt to access the same variable at the same time. For example, consider two threads that access the same collection. The first thread might add an object to the collection. The second thread then might remove an object from the collection based on the index of the object. The first thread then might attempt to access the object in the collection to find that it had been removed. Race conditions can lead to unpredictable effects that can destabilize your application.
The best way to avoid race conditions and deadlocks is by careful programming and judicious use of thread synchronization. You can use the SyncLock keyword in Visual Basic and the lock keyword in C# to obtain an exclusive lock on an object. This allows the thread that has the lock on the object to perform operations on that object without allowing any other threads to access it. Note that if any other threads attempt to access a locked object, those threads will pause until the lock is released. The following example demonstrates how to obtain a lock on an object:
Sample of Visual Basic.NET Code
SyncLock anObject ' Perform some operation End SyncLock
Sample of C# Code
lock (anObject) { // Perform some operation }
Some objects, such as collections, implement a synchronization object that should be used to synchronize access to the greater object. The following example demonstrates how to obtain a lock on the SyncRoot object of an ArrayList object:
Sample of Visual Basic.NET Code
Dim anArrayList As New System.Collections.ArrayList SyncLock anArrayList.SyncRoot ' Perform some operation on the ArrayList End SyncLock
Sample of C# Code
System.Collections.Arraylist anArrayList = new System.Collections.ArrayList(); lock (anArrayList.SyncRoot) { // Perform some operation on the ArrayList }
It is generally good practice when creating classes that will be accessed by multiple threads to include a synchronization object that is used for synchronized access by threads. This allows the system to lock only the synchronization object, thus conserving resources by not having to lock every single object contained in the class. A synchronization object is simply an instance of Object and does not need to have any functionality except to be available for locking. The following example demonstrates a class that exposes a synchronization object:
Sample of Visual Basic.NET Code
Public Class aClass Public SynchronizationObject As New Object() ' Insert additional functionality here End Class
Sample of C# Code
public class aClass { public object SynchronizationObject = new Object(); // Insert additional functionality here }
Special Considerations When Working with Controls
Because controls are always owned by the UI thread, it is generally unsafe to make calls to controls from a different thread. In WPF applications, you can use the Dispatcher object, discussed later in this section, to make safe function calls to the UI thread. In Windows Forms applications, you can use the Control.InvokeRequired property to determine if it is safe to make a call to a control from another thread. If InvokeRequired returns False, it is safe to make the call to the control. If InvokeRequired returns True, however, you should use the Control.Invoke method on the owning form to supply a delegate to a method to access the control. Using Control.Invoke allows the control to be accessed in a thread-safe manner. The following example demonstrates setting the Text property of a TextBox control named Text1:
Sample of Visual Basic.NET Code
Public Delegate Sub SetTextDelegate(ByVal t As String) Public Sub SetText(ByVal t As String) If TextBox1.InvokeRequired = True Then Dim del As New SetTextDelegate(AddressOf SetText) Me.Invoke(del, New Object() {t}) Else TextBox1.Text = t End If End Sub
Sample of C# Code
public delegate void SetTextDelegate(string t); public void SetText(string t) { if (textBox1.InvokeRequired) { SetTextDelegate del = new SetTextDelegate(SetText); this.Invoke(del, new object[]{t}); } else { textBox1.Text = t; } }
In the preceding example, the method tests InvokeRequired to determine if it is dangerous to access the control directly. In general, this will return True if the control is being accessed from a separate thread. If InvokeRequired does return True, the method creates a new instance of a delegate that refers to itself and calls Control.Invoke to set the Text property in a thread-safe manner.
Using Dispatcher to Access Controls Safely on Another Thread in WPF
At times, you might want to change the UI from a worker thread. For example, you might want to enable or disable buttons based on the status of the worker thread, or provide more detailed progress reporting than is allowed by the ReportProgess method. The WPF threading model provides the Dispatcher class for cross-thread calls. Using Dispatcher, you can update your UI safely from worker threads.
You can retrieve a reference to the Dispatcher object for a UI element from its Dispatcher property, as shown here:
Sample of Visual Basic.NET Code
Dim aDisp As System.Windows.Threading.Dispatcher aDisp = Button1.Dispatcher
Sample of C# Code
System.Windows.Threading.Dispatcher aDisp; aDisp = button1.Dispatcher;
Dispatcher provides two principal methods that you will use: BeginInvoke and Invoke. Both methods allow you to call a method safely on the UI thread. The BeginInvoke method allows you to call a method asynchronously, and the Invoke method allows you to call a method synchronously. Thus, a call to Dispatcher.Invoke will block execution on the thread on which it is called until the method returns, whereas a call to Dispatcher.BeginInvoke will not block execution.
Both the BeginInvoke and Invoke methods require you to specify a delegate that points to a method to be executed. You also can supply a single parameter or an array of parameters for the delegate, depending on the requirements of the delegate. You also are required to set the DispatcherPriority property, which determines the priority with which the delegate is executed. In addition, the Dispatcher.Invoke method allows you to set a period of time for the Dispatcher to wait before abandoning the invocation. The following example demonstrates how to invoke a delegate named MyMethod using BeginInvoke and Invoke:
Sample of Visual Basic.NET Code
Dim aDisp As System.Windows.Threading.Dispatcher = Button1.Dispatcher ' Invokes the delegate synchronously aDisp.Invoke(System.Windows.Threading.DispatcherPriority.Normal, MyMethod) ' Invokes the delegate asynchronously aDisp.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, MyMethod)
Sample of C# Code
System.Windows.Threading.Dispatcher aDisp = button1.Dispatcher; // Invokes the delegate synchronously aDisp.Invoke(System.Windows.Threading.DispatcherPriority.Normal, MyMethod); // Invokes the delegate asynchronously aDisp.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, MyMethod);
Avoiding Unnecessary Screen Refreshes
Client applications refresh the screen when the visible contents of a control are updated. Refreshing the screen is processor-intensive, and can negatively affect the performance of applications such as Remote Desktop, which transfer the UI across the network. While most Windows applications will work fine without any special planning, there are several best practices you can follow to avoid unnecessary screen refreshes:
Reduce the default animation rate of 60 frames per second (fps). The following code sample sets it to 20 fps:
Sample of Visual Basic.NET Code
Timeline.DesiredFrameRateProperty.OverrideMetadata( _ GetType(Timeline), _ New FrameworkPropertyMetadata() With {Key.DefaultValue = 20})
Sample of C# Code
Timeline.DesiredFrameRateProperty.OverrideMetadata( typeof(Timeline), new FrameworkPropertyMetadata { DefaultValue = 20 } );
Reduce the animation rate for a Storyboard or individual objects within a Storyboard. By default, animations run at 60 fps. The lower the value of the Timeline.DesiredFrame Rate attached property, the better your performance will be.
WPF will refresh controls automatically when you specify a DependencyProperty or implement INotifyPropertyChanged. Avoid rapidly updating values that will initiate a refresh; for example, by updating the value within a loop. Instead, update the value once after the loop has completed.
Determining Whether to Sort and Filter Data on the Client or Server
Many tasks can be performed on either the client or the server. The most important considerations are as follows:
- Security Security-related tasks, such as authentication, authorization, auditing, and data validation, always must occur on the server. Client applications should never be trusted.
- Responsiveness Perform tasks on the client to provide the best responsiveness. For example, imagine a WPF application that retrieves a list of products from a WCF web service. If the user wants to sort the list differently, the WPF application could re-sort the list in memory, or it could send a second request to the web service to retrieve the list in a different order. Sending the request to the web service incurs a delay, however, because the request and response must be sent across the network. The higher the network latency, the longer the delay.
- Client and server load Performing tasks on the client increases the load on the client, and performing tasks on the server increases the load on the server. For clients with sufficient processing power, perform the task on the client to improve server scalability. If your client computers do not have the processing power necessary to perform a task in a reasonable time, perform the task on the server.
- Network utilization Every task that you perform on the server requires data to be sent from the client to the server. This increases network utilization. While it might be difficult to notice the impact on a higher-performance local area network (LAN), wireless and wide area network (WAN) links can be saturated much more easily, slowing the performance of every application running on the network.
As a general rule, process non-security Presentation layer tasks on the client, and process Business Logic layer tasks on the server. If a particular Business Logic layer task would be much faster to process on the client, then create a separate client-side Business Logic layer assembly, and perform that processing on the client (but validate the data on the server).
Addressing UI Memory Issues
UI elements can consume a great deal of memory if not used carefully. Follow these best practices to avoid common UI-related memory leaks in WPF:
Un-register event handlers from a child window to a parent window when you no longer need them. Otherwise, the child window will remain in memory until the parent window is closed, even if the user closes the child window.
Un-register event handlers to static objects when you no longer need them. Static objects always stay alive in memory.
Stop Timer objects when you no longer need them.
Set the TextBox.UndoLimit property if you plan to update a TextBox repeatedly.
If a data-binding target refers back to the class that is bound to it, manually remove the binding before closing the window. For detailed information, refer to Microsoft Knowledge Base article 938416 at http://support.microsoft.com/kb/938416/.
Freeze objects whenever possible. Freezing an object disables change notifications, improving performance and reducing memory usage. For example, if you create a brush to set the background color of an object, the .NET Framework must monitor that brush for changes so that the changes can be reflected in any objects that use the brush. Freezing the brush would spare the .NET Framework from having to do that task. Freezable objects derive from the Freezable class and are usually graphics-related, including bitmaps, brushes, pens, and animations. Before freezing an object, verify that the value of the CanFreeze property is true. For detailed information, read “Freezable Objects Overview” at http://msdn.microsoft.com/library/ms750509.aspx.
Use the CLR Profiler, described in Objective 5.3 in Chapter 5, to identify memory leaks. If you examine the working set using Task Manager or Performance Monitor, you will see an exaggerated memory size. To get a more realistic size, minimize your .NET Framework application. The CLR Profiler is available as a free download at http://www.microsoft.com/download/en/details.aspx?id=16273.
Objective Summary
The BackgroundWorker component encapsulates a worker thread and provides methods to report progress from that thread.
You can create new threads directly using the Thread object.
When using threads that communicate directly with the UI thread, you must take care to avoid cross-thread function calls. In Windows Forms interfaces, query the Control.InvokeRequired property to determine if Invoke is required. In WPF, use the Dispatcher object to communicate safely with the UI thread.
Objective Review
Answer the following questions to test your knowledge of the information in this objective. You can find the answers to these questions and explanations of why each answer choice is correct or incorrect are located in the “Answers” section at the end of the chapter.
Which of the following are required to start a background process with the BackgroundWorker component? (Choose all that apply.)
Calling the RunWorkerAsync method
Handling the DoWork event
Handling the ProgressChanged event
Setting the WorkerSupportsCancellation property to True
Which of the following are good strategies for updating the UI from the worker thread? (Choose all that apply.)
Use Dispatcher.BeginInvoke to execute a delegate to a method that updates the UI.
Invoke a delegate to a method that updates the UI.
Set the WorkerReportsProgress property to True, call the ReportProgress method in the background thread, and handle the ProgressChanged event in the main thread.
Call a method that updates the UI from the background thread.