Performance Design Strategies for User Interface CodeUser interface code is complex because it is the boundary where your code interacts with both the operating system and the end user. Your user interface literally sits in between and brokers requests connecting the user and the operating system. As an application designer, you face myriad choices:Which standard or custom controls should be used to display data and interact with the user?Does it make sense to build your own custom controls or extend the behavior of existing controls to meet special needs?What strategies should be used for populating user interface elements that hold a lot of data? Controls containing lists or trees of data are of particular importance.Which control events should be responded to? When should this response be suppressed?Is there a way to reduce the number of events that get fired when common or repetitive tasks occur?Is it necessary to force the immediate update or repainting of a control for short-term user experience gain at the cost of longer-term performance? Because of all these dimensions of choices, the possible code implementations to solve a specific user interface need are almost endless. This is liberating but at the same time it also allows the possibility of haphazard and inefficient design. It is as if you went to the furniture store to buy a new couch and they handed you several bags of parts, more than you need for any single couch, and gave you no instructions but instead sent you off to create your own couch. Maybe you end up with a great-looking and sturdy couch, or maybe you end up with something less structurally sound that resembles the primitive shelter building efforts of early man. Without instructions, a strategy and good construction guidelines, odds are you will end up with the latter. With a good design plan, you will get the former. As with our fictitious couch, after you put enough pieces together, it becomes hard to take them all apart again and revise your design. If you want to avoid a substandard result or the need to start again from scratch, you need some design strategies to guide you and ensure your application's user interface development stays on the right path.Below are covered some of the main design strategies for performance-oriented user interface design. Use the Built-In Performance FeaturesIf the mobile application runtime you are using has specific features to help build well-performing user interface code, you should use them. Moreover, you should actively review the list of events, properties, and methods for the controls you use and look for specific performance-oriented features. This seems obvious but is very often overlooked. The best way to learn about the performance-oriented features of a user interface framework is to examine members of its window, form, or control classes. It is also useful to examine the overloaded versions of methods that exist to perform common tasks to see whether there are methods that enable you to perform operations in a batched mode rather than by calling a method over and over again in a loop. As you would expect, batched mode operations are typically faster. This review usually only takes a few minutes, and you may be surprised at what you discover; lots of poor interface code is simply the result of using inefficient properties or methods to get things done or not calling methods that suspend user interface actions when updates are being performed. Example: Performance Differences in Varying Approaches for Working with a TreeView ControlThis code in Listing 11.1 measures the efficiency of populating a .NET Compact Framework TreeView control using three different techniques. To build the sample, use Visual Studio .NET to create a new C# mobile device project and select the Pocket PC as your target platform. Onto the blank Form designer, add a TreeView control and five buttons as shown in Figure 11.1. Figure 11.1. The Visual Studio .NET Form Designer with TreeView and Button controls.[View full size image] ![]()
Listing 11.1. Populating and Clearing a TreeView Control Using Alternative StrategiesThe results of various approaches to adding and removing items from the TreeView control are listed in Table 11.1 and Table 11.2.
Test with the Actual Number of Elements You Will Display in Your ApplicationA common design mistake encountered when writing user interface code is to design and test with smaller sets of data than will actually be encountered in the mobile application's deployment. An algorithm that works fine retrieving 20 items from a database and populating a user interface with them may or may not work well with 200 items. If you are using smaller sets of data for your testing, you are leaving the door open for nasty surprises later in the development or deployment cycle when the real-world data comes in. It is far better to discover these kinds of performance issues earlier when creative design strategies can be explored for mitigating them; late design changes will always be harder to implement, riskier to application stability, and usually produce less-satisfying results.Another common mistake is using different data sources during the design of an application. For example, using a local database or text file during design as stop gap until the real data becomes available on the remote server the mobile application will actually have to access. Most often these kinds of issues occur either when the real databases and data are not yet available for use at application design time or when the data is sensitive and live data is not available to the application designers for testing purposes. Similar kinds of performance shocks can occur when data sources are switched. As above it is best test early with real-world conditions.In any case, it is important to specify the capacity, location, and access mechanism for the data your application will display in its user interface. Do this early in the design process and test with data of this size, location, and access mechanism. If your application is intended to work with a large variety of data sizes, test with the largest capacity. It is highly recommended to give yourself some performance buffer and test with data capacities that exceed your design specification by a reasonable amount and see how the application's performance is affected. If your data can be pulled from different sources, test the performance using each data-access mechanism. If your application will need to access the data via a dialup or lower-bandwidth connection, test it under these conditions. Not only should the application run correctly in these situations, it is also important that your application remain visually attractive and behave responsively throughout any prolonged user interface update. Doing these things will keep your design process focused on the real-world performance you will need to achieve. There simply is not a substitute for working with same data in the same way that your end users will. Procrastination Is Good! Defer, Defer, Defer...Many applications serve to give the user a condensed view onto a large amount of dynamic data. The data may be stored locally in a file or database, or pulled dynamically down from a network as needed.For example, a mobile application for real estate agents may give a view onto hundreds or thousands of properties and organize them in a hierarchy navigable by the application's user via a mobile phone or a PDA. A first-iteration design to achieve this goal might typically load all the skeletal information about available house listings into memory and enable the user to navigate the hierarchy and find potential properties. When a specific property is selected the application would then load details such as photos, floor plans, street maps, and the market status for the property on demand.For the top-down navigation of housing choices, a TreeView control can offer rich hierarchical view on the possible property listings. For instance, a TreeView control could be used to offer three top-level nodes, each representing a different pivot of the data based on a probable decision process that an end user might use to navigate the data. Below are three examples of listing hierarchies that the application's users might want:Navigation 1 Start with the neighborhood, and then look at price groupings to determine which units to show. TreeView hierarchy: Neighborhoods->Price->ListOfUnits.Navigation 2 Start with the price groupings, and then look at house types and finally the neighborhood to arrive at the list of units to show. TreeView hierarchy: Price->HouseType-> Neighborhood->ListOfUnits.Navigation 3: Start with the type of house, and then look at price ranges and then the neighborhoods. TreeView hierarchy: HouseType->Price->Neighborhood->ListOfUnits. Figure 11.2 shows what this TreeView navigation might look like. Three top-level nodes Neighborhood, Price, and HouseType exist. Each parent node has subnodes enabling users to drill down through the hierarchy and finally arrive at the set of houses they want to look at. Figure 11.2. A simple example of what TreeView real estate navigation could look like.![]() If we populate the TreeView in advance of the application user's navigation, we will end up creating and populating a huge number of tree nodes, most of which users will never navigate to in any given application usage session. Creating all these TreeView members takes time, and the TreeView item nodes themselves consume system resources. Holding thousands of TreeView items in memory would have a detrimental effect on our mobile application's performance. Unless we think the user is going to visit each of these nodes individually and repeatedly, we can achieve better performance by using a more sophisticated approach. What we would like to do is keep the TreeView user interface metaphor but avoid creating hundreds or thousands of unvisited TreeView items. A way to accomplish this is to create the needed TreeView nodes only when it becomes clear that the user will access them. This can be done by using some clever code to (1) populate the TreeView control only up until the point where it has been expanded to, and (2) handle the event that indicates a node of the TreeView control is about to be expanded and needs to be populated with valid data. Doing this can save the mobile application the time and resources required to fill a huge amount of nodes and will result in a better end user experience.More sophisticated approaches such as looking ahead one level deep in the TreeView and prepopulating the nodes are also possible; this could potentially provide the user an even richer experience. The richness of the approach you choose to use to intelligently defer work is bounded only by the work required to design and implement your concepts. Example: On-Demand Population of a TreeView ControlFigure 11.3 shows a simple application consisting of a TreeView control (treeview1) and a Button (button1). Clicking the button at runtime sets up or resets the state of the TreeView control. After the TreeView control has been set up by clicking the button, it offers three top-level nodes that can be dynamically populated. The nodes are Neighborhoods, Prices and HouseTypes. To keep the code size small and the sample simple, only the code to dynamically populate the Neighborhoods node is included in the Listing 11.2. Clicking the other nodes will bring up a MessageBox the first time they are clicked to show where the dynamic TreeView population should occur and you are free to add your own code here. Figure 11.3. Pocket PC emulator with an application that dynamically populates a TreeView control.![]()
Listing 11.2. Dynamic Population of TreeView ControlThe code above shows how, with some cleverness and only a bit of additional code, complex user interface population can be deferred until the user has a need for the data. Whether working with the .NET Compact Framework or other device runtimes, learning how to defer the population of expansive user interface elements is a powerful technique. Keep a Careful Eye on Event-Driven CodeA large portion of modern user interface code consists of code that responds to framework-generated events. Knowing which events to handle, how often the events are triggered, and when to avoid responding to events is important for designing user interface code that performs well.A common situation that causes poor application performance is mistakenly handling events generated by code believing that the events are user generated. Example: Showing the TextBox "Changed" Event Being Triggered When the .Text Property Is SetListing 11.3 contains the code that needs to be inserted into a Form class for this example. To create the sample, follow these steps:
NoteProgrammatically setting the text property of the TextBox actually triggers the TextChanged event twice when running on the .NET Compact Framework Version 1.1.Running the same code on the desktop triggers the event only once. It is likely that in the future releases the .NET Compact Framework behavior will be changed to match the desktop .NET Framework behavior (only one event triggering). Events can be tricky stuff. Keep an eye on how and when events fire. Listing 11.3. Programmatic TextBox Update Causes Event Code to Be RunListing 11.3 shows that programmatically setting the Text property of a TextBox fires the same event code as a user typing in text. Depending on your assumptions, this may or may not be what you expected. Often people write code to populate user interfaces after retrieving data from some external source. RadioButton and CheckBox buttons have their Checked properties set, TextBox values are filled, ListBox and ComboBox contents are filled, and so on. It is often assumed that this setup work does not trigger user interface events. Usually the intent is not to want these events to be triggered as the user interface is just being set up for the user to use. Very often the only time an application's programmer wants the application's event handler code to be run is when an external event occurs such as a timer ticking, a user clicking a button, or a user entering text into a control. Avoiding Getting Caught Off-Guard by EventsEvent code is tricky because it is not possible to tell by inspection when an application's event handling code will be called. Examining an application's source code shows clearly when calls are made to runtime libraries and to other application code, but does not indicate when the runtime triggers application events. For this reason, most developers simply make assumptions about when events are triggered and do not check these assumptions closely. In addition, as an application grows in user features and correspondingly in internal complexity more event handling and event generating user interface code gets written; this code may have subtle interactions that are not apparent by inspection. For example, code that responds to selecting an item in a ListBox may run code that causes an update to a CheckBox's and a TextBox's properties. For example: This code seems simple, but if events are hooked up to the TextBox or CheckBox controls they may be triggered and may run application code meant only to process end-user actions. This code may in turn change other user interface elements and cause a cascade of events unexpected by the application's developer.Here are three good ways to help ensure that your application does not get caught off-guard with events:Instrument application code during development. Counting the number of times an event gets fired is a low-overhead activity; it only requires incrementing an integer. It can also be diagnostically useful to keep an ordered list of events that occur and examine these. This instrumentation can be conditionally compiled into your application. It is a good idea to periodically audit your application's behavior throughout the development cycle and examine what a highly instrumented version tells you. Instrumentation is easy and remarkably effective.Have a clear state model for when events should run code and when they should immediately exit. A common case where event code is unintentionally run is when the application's user interface is being populated with data. In these cases, the desire is simply to push data into the user interface and not to respond to events triggered by this push. An easy way to deal with this is to set a Form-level flag that states "the application is doing user interface updates now, event handlers should exit without processing." Each event handler should in its first line of code check the state of this flag and if it is set, exit without further processing.Periodically place breakpoints in each of the applications event handlers and walk through the sequence of events that execute. Periodically it is useful to single step through the set of events that fire in your user interface and see what processing is going on and in what order. You may find event cascades that are inadvertently being caused when the user interface is updated by application code; you may also find redundant processing you are doing that is costly. Event code that causes multiple redundant reads from or updates to a database is a good example of code that should be discovered and eliminated.Event-driven code is a wonderfully useful concept, but like any abstraction it comes at a cost of reduced low-level understanding of what is going on. Unless you understand the sequence and frequency of events that are causing your application's code to run, you cannot truly say that you understand the behavior of your application. This can cause not only performance problems but also reliability problems. It is well worth spending the time to get a firm and detailed understanding of where and when the events in your application are firing.Figure 11.4 and Listing 11.4 show a sample application that demonstrates both instrumentation and using a state model to prevent user interface event code from running when programmatic updates to the application's user interface are occurring. To create the sample, follow these steps:
Listing 11.4. Using a State Model for Updates and Instrumentation to Better Understand and Control Event Processing
Figure 11.4. Pocket PC showing an application with event-logging instrumentation.![]() Never Leave the User GuessingResponsiveness is a significant aspect of performance. It is always best to have no delays in the user interface's interactions with the user, but in some cases this may not be possible. When work must be done that forces the end user to wait, extra effort should be applied to make sure that the wait is as comfortable as possible. Think of it as similar to designing a nice comfortable room in your doctor's office. It would be great if you did not need to wait at all, but this seems not to be possible. (If you know of an exception, let me know!) It would be a much worse experience if there were no receptionist to acknowledge your arrival, if you were never told how long you will need to wait, if the surroundings were uncomfortable, and if there were nothing to look at or read to pass the time. Having a fish tank in the room to look at while you wait is an aesthetically nice distraction. Having the receptionist acknowledge your arrival goes a long way as well. The experience is even better if the receptionist tells you how long the wait will be and offers you today's newspaper and a beverage of your choice to keep you comfortable. (Again, if you know of a doctor's office where this occurs, let me know.)Chapter 7, "Step 1: Start with Performance, Stay with Performance," outlines several different levels of user responsiveness and offers an example that shows them. To review this and offer some more detail, the different levels of end-user experience when waiting for a mobile application are as follows:The worst experience: No responsiveness. An application that becomes unresponsive and does not offer users any indications that work is being done on their behalf looks an awful lot like an application that has crashed or locked up. The end user anxiety level will quickly climb and they will start attempting to click buttons or tap on the screen to get the application to respond in some way. This leaves a series of random unintended events for your application to process when it returns control back to the user interface. After a short while, the user will probably try to close the application, switch to another application, or yank out the battery of the device to try to reset it if the former strategies do not yield acceptable responses. (Mobile phones make this very easy to do.) Making this situation worse is having a mobile application that does not give the user any visual indication when the work is complete. Assuming users have patiently waited and did not push any buttons or yank out the battery while the application was stalled, how are they supposed to know when the application is responsive again? In all cases, avoid having a user interface that looks like it should be responding to requests but is not.A better experience: Showing a wait cursor while the application is unresponsive. A wait cursor serves two important purposes. First, it lets end users know work is being done on their behalf, and second, it lets them know when the work is completed so they can resume using the application. Given that showing a wait cursor is usually a trivial task, there is really no excuse not to at least do this small bit of responsiveness work. Any action that takes more than a few tenths of a second is a user interface stall that the user will notice and there is little reason not to let them know that work is being done on their behalf. A wait cursor will buy you a few seconds of grace and patience from the device's user, but if the work is going to take more than a few seconds you should look at having a more informative progress indicator in addition to the wait cursor. Additionally, if almost every action the user does results in a wait cursor length delay, you should look more at optimizations and background processing for your mobile application's design; delays are fine for some things but not for almost everything.An even better experience: Showing the user a progress bar or displaying progress text that indicates what is going on. If the action the user needs to wait for will take more than a few seconds, consider having an updating progress bar or progress text that informs the user of progress that is occurring, the percent of work remaining or an updating estimate of the time remaining to completion. Showing a wait cursor does not prevent you from displaying text that informs the user of the progress of the work. You may want to display some text on the form behind the wait cursor that tells the user what is being done on their behalf. Any information you can give the user that indicates progress is useful. Informative text can be particularly useful in keeping users feeling like they are part of the process. For example, an algorithm that downloads information from a server might display the following incremental progress updates: "Locating server," "Found server, logging in," "Downloading information (10%)," "Downloading information (60%)," "Done!" This kind of information makes the user feel involved in the process. Note the importance of telling them that the action has completed successfully. Further, if there are problems during the download, users may be able to glean information from the progress text that can be used to resolve the problem.When writing code that periodically updates the user on the progress of a long-running task, you may need to explicitly make the control draw itself. Unless explicitly called to update, forms and controls usually only queue up messages that request they be redrawn; unforced updates occur when the system has spare time to process them. Because your computational work may be occurring on the same thread as the user interface, the work being done will not allow the queued messages to be processed until control is handed back to the user interface. To solve this the .NET Compact Framework supports an Update() method on each control. When this method is called, the control is repainted immediately. Listing 11.5 shows a simple work algorithm that periodically updates the text in a Label control to show progress. Unless a call to label1.Update() is made when the Label's text is updated, the new text will not display to the user until after the work is complete.A better experience yet: Giving the user progress information and the ability to cancel the operation. Some tasks may take a significant amount of time, and the user may want to cancel them before they complete. A synchronization task with a server might unexpectedly take several minutes, and the mobile device's user may decide that they need to enter an elevator or go underground to a subway where there is no network access. Alternatively, the end user of the mobile device may need to get to information in another part of the application quickly and cannot wait for the task currently underway to complete. If a task is going to take a long time and is going to block the user interface, it is good design to offer the user the ability to cancel the action being performed on his behalf. This can be done by having your algorithm periodically poll for a flag that is set if the user presses a cancel button. As the chapter on background threading explained, a state machine governing the background task's status is a good way to implement this.If the long-running task is being performed on the foreground (user interface) thread, offering the user the ability to cancel is more complicated but still possible. As with updating the user interface during heavy processing, responding to user requests such as clicking a cancel button cannot happen unless the thread's user interface messages are being processed. The .NET Compact Framework offers a solution to this and other mobile device frameworks are likely to have a similar concept. Making a call to the static method System.Windows.Forms.Application.DoEvents() forces all the queued up messages on the thread to be processed before execution continues. This means that user interface painting messages will be processed and this also includes processing queued-up button click and key press messages. If you call DoEvents() on a reasonably frequent bases (a few times a second), you can keep the application's user interface responsive to user requests while performing the processing task at hand.Calling DoEvents() to process a thread's queued-up user interface messages seems like a wonderful cure-all, but it is not. Extreme caution must be used when calling DoEvents() because subtle and highly undesirable effects can arise. Because DoEvents() processes all messages before returning control to the code that called it, things such as timer event handlers can get called and running event handlers can be re-entered as well. It is possible to end up with multiple nested calls into the event handlers for user interface elements and timers if DoEvents() is called inside an event and a new event message has been queued up before the first one is completed. If you are going to use DoEvents(), you must write your application's event handling code very defensively. Putting checks at the top of your event handlers to exit the function if it is being re-entered is highly recommended. Before entering a block of code that will perform a long-duration task and will call DoEvents() in the middle of it, be sure to set the Enabled property of any controls that should not be allowed to generate events to false; this will disable the user from being able to click them and queue up events. There are almost no cases where re-entrant events are desirable; the code gets very confusing to understand and debug. Given the choice between using DoEvents() and creating a background thread to do processing, I would almost always choose a well-designed background threading solution. As complicated as multiple threads are to conceptualize, their execution model is usually simpler and more predictable than the mixture of framework and application code that can get called when using DoEvents(). None of this is specific to mobile devices; the same issues exist on the desktop but given the user's demand for responsiveness in mobile devices it is worth drawing out this issue specifically. Nevertheless DoEvents() can occasionally be useful and I describe it here along with the warning caveat emptor (let the buyer beware!). If you buy the DoEvents() ticket, you will take the ride with all of its ups and downs.The best experience: Find a way to perform the task in a way that does not block the user. If your mobile application can push work onto a background thread, enabling users to continue with other tasks and give them a notification when the task is complete, you have achieved the Holy Grail of responsiveness. Doing this requires a great deal of thought about what the user interaction model is for pushing work onto a background thread. Thought must be given to how to keep the user notified of progress and in control of the background processing without being too intrusive to the foreground user interface experience. A good desktop application example of this would be Microsoft Word's support for printing a document in the background while enabling the user to continue to edit the live document in the foreground. It is a useful feature and looks simple, but there is significant design behind it to make it work. Listing 11.5. Calling a Control's Update() Method to Show Progress Text
|