Charles Petzold

A DataTemplate is most commonly created in conjunction with an ItemsControl or a class that derives from ItemsControl, which includes ListBox, ComboBox, Menu, TreeView, ToolBar, StatusBar—in short, all the controls that maintain a collection of items. The DataTemplate defines how each item in the collection is displayed. The DataTemplate consists mostly of a visual tree of one or more elements, with data bindings that link the items in the collection with properties of these elements. If the items in the collection implement some kind of property-change notification (most often by implementing the INotifyPropertyChanged interface), the Items-Control can dynamically respond to changes in the items.
The disappointment might come a little later. If you need to display lots of data, you might discover that the ItemsControl and DataTemplate don't scale well. This column is about what you can do to combat those performance issues.

An ItemsControl Scatter Plot
Let's create a scatter plot from an ItemsControl and a DataTemplate. The first step is to create a business object representing the data item. Figure 1 shows a simple class with the generic name DataPoint (slightly abridged). DataPoint implements the INotify­PropertyChanged interface, which means that it contains an event named PropertyChanged that the object fires whenever a property has changed.
Writing More Efficient ItemsControls Figure 1 The DataPoint Class Represents a Data Item
The VariableX and VariableY properties indicate the point's position in a Cartesian coordinate system. (For this column, the values range from 0 to 1.) The Type property can be used to group data points (it will have values from 0 through 5 and be used to display information in six different colors), and the ID property identifies each point with a text string.
In a real-life application, the following DataCollection class might contain more properties, but for this example it has only one, DataPoints, of type ObservableCol­lection<DataPoint>:
public class DataCollection {     public DataCollection(int numPoints)     {         DataPoints = new ObservableCollection<DataPoint>();         new DataRandomizer<DataPoint>(DataPoints, numPoints,                                            Math.Min(1, numPoints / 100));     }      public ObservableCollection<DataPoint> DataPoints { set; get; } } 
ObservableCollection has a CollectionChanged property that is fired whenever items are added to or removed from the collection.
This particular DataCollection class creates all the data items in its constructor by using a class named DataPointRandomizer that generates random data for testing purposes. The DataPointRandomizer object also sets a timer. Every tenth of a second, the timer-tick method changes the VariableX or VariableY property in 1% of the points. Therefore, on average, all the points change every 10 seconds.
Now let's write some XAML that displays this data in a scatter plot. Figure 2 shows a UserControl that contains an ItemsControl. The DataContext of this control will be set in code to an object of type DataCollection. The ItemsSource property of the ItemsControl is bound to the DataPoints property of the DataCollection, which means that the ItemsControl will be filled with items of type DataPoint.
Writing More Efficient ItemsControls Figure 2 The DataDisplay1Control.xaml File
The ItemTemplate property of the ItemsControl is set to a DataTemplate that defines a visual tree for the display of each item. This tree consists of just a Path element with its Data property set to an EllipseGeometry. It's really just a little dot—and I mean a very little dot—with a radius of 0.003 units. But it's not as tiny as it seems because the ItemPanel property of the ItemsControl is set to an ItemsPanelTemplate containing a single-cell Grid to display all the dots. This Grid is given a LayoutTransform that scales it by a factor of 300, which makes the data dots almost 1 unit in radius.
This odd manipulation is mandated by the data bindings of the VariableX and VariableY properties to the X and Y properties of a TranslateTransform. The VariableX and VariableY properties only range from 0 to 1, so it's necessary to increase the size of the Grid to occupy an area on the screen 300 units square.
The Fill property of the Path element is set in a Style to a Brush based on the value of the Type property in each DataPoint object. The Path object is also assigned a ToolTip that displays the information about each data point.

The Performance Disappointment
The source code that accompanies this column consists of one Visual Studio solution containing seven projects: six applications and one library. The library named DataLibrary contains most of the shared code, including DataPoint, DataCollection, and DataPointRandomizer.
The first application project is named DataDisplay1. It contains a MainWindow class that is shared among the other applications, and the DataDisplay1Control.xaml file shown in Figure 2. MainWindow accesses the control for displaying the scatter plot but also includes a TextBox to enter an item count, a button to begin creating the collection, and TextBlock objects that display elapsed time during three stages of the processes that culminate in the display of the chart.
By default, the number of items is 1,000, and the program seems to work fine. But set the number of items to 10,000, and there's a delay before the display shown in Figure 3, which also illustrates very much the artificiality of the generated data.
Writing More Efficient ItemsControls
Figure 3 The DataDisplay1 Display
Three elapsed times are displayed. When you click the button, the Click handler in MainWindow.xaml.cs begins by creating an object of type DataCollection with the specified number of data points. The first elapsed time is for the creation of this collection. The collection is then set to the DataContext of the window. That's the second elapsed time. The third elapsed time is the time required for the ItemsControl to display the resultant data points. To compute this third elapsed time, I used the LayoutUpdated event for lack of a better alternative.
As you can see, the bulk of the time involves updating the display; on my machine the average of three trials was 7.7 seconds. This is rather disturbing, particularly considering that this program really has nothing wrong with it. It is using WPF features in the way they were intended. What exactly is going on here?
When the program sets the DataContext property to an object of type DataPointCollection, the ItemsControl receives a property-change notification for the ItemsSource property. The ItemsControl enumerates through the collection of DataPoint objects, and for each DataPoint object it creates a ContentPresenter object. (The ContentPresenter class derives from FrameworkElement; this is the same element that ContentControl derivatives such as Button and Window use to display the control's content.)
For each ContentPresenter object, the Content property is set to the corresponding DataPoint object, and the ContentTemplate property is set to the ItemTemplate property of the ItemsControl. These ContentPresenter objects are then added to the panel used by the ItemsControl to display its items—in this case, a single-cell Grid.
This part of the process goes fairly quickly. The time-consuming part comes when the ItemsControl must be displayed. Because the panel has accumulated new children, its MeasureOverride method is called, and it is this call that requires 7.7 seconds to execute for 10,000 items.
The panel's MeasureOverride method calls the Measure method of each of its ContentPresenter children. If the ContentPresenter child has not yet created a visual tree to display its content, it must now do so. The ContentPresenter creates this visual tree based on the template stored in its ContentTemplate property. The ContentPresenter must also set up the data bindings between the properties of the elements in that visual tree and the properties of the object stored in its Content property (in this example, the DataPoint object). The ContentPresenter then calls the Measure method of the root element of this visual tree.
If you're interested in exploring this process in more detail, the DataLibrary DLL includes a class named SingleCellGrid that lets you probe inside the panel. In the DataDisplay1Control.xaml file, you can just replace Grid with data:SingleCellGrid.
When a ListBox contains many items but displays only a few, it bypasses a lot of this initial work because by default it uses a VirtualizingStackPanel that creates children only as they are being displayed. This is not possible with a scatter plot, however.

Hidden Loops
The loop is the fundamental construct of computer programming. The only reason we use computers is to write loops that perform repetitive chores. Yet loops seem to be disappearing from our programming experience. In functional programming languages such as F#, loops are relegated to old-style programming and are often replaced with operations that work over entire arrays, lists, and sets. Similarly, query operators in LINQ perform operations over collections without explicit looping.
This move away from explicit looping is not just a change in programming style but an essential evolutionary development to keep pace with computer hardware changes. New computers today routinely have two or four processors; in the years ahead we may see machines with hundreds of processors that perform in parallel. Loops that are handled behind the scenes in programming languages or development frameworks can more easily take advantage of parallel processing without any special work by the programmer.
The DataTemplate defined within the ItemTemplate section of an ItemsControl is inside a hidden loop. That DataTemplate is invoked for the creation of potentially thousands of elements and other objects, as well as the establishment of data bindings.
If we actually had to code the loop, we would all probably be much more careful designing that DataTemplate. Because of its impact on performance, fine-tuning the DataTemplate is well worth the time and effort. Just about anything you do to it will probably have a perceptible impact on performance. Often, just how much impact (and in what direction) can be hard to predict, so you'll probably want to experiment with several approaches.
In general, you'll want to simplify the visual tree in the DataTemplate. Try to minimize the number of elements, objects, and data bindings.
Go to DataDisplay1Control.xaml and try eliminating the data bindings on the TextBlock items in the ToolTip. (You can simply insert any character in front of the left curly brackets.) You should shave a couple tenths of a second off the previous time of 7.7 seconds.
Now comment out the whole ToolTip section, and you'll see the display time drop down to 4.7 seconds. Set a Fill property in the Path element to some color, and comment out the whole Style section, and now the display time drops down to 3.5 seconds. Remove the transform from the Path element, and it will come down to about 1 second.
Of course, now it's worthless because it's not displaying the data, but you can probably see how you can begin to get a feel for the impact of these items. It's just a little bit of markup, but it's worth a lot of experimentation.
Here's a change that improves both performance and readability without hurting functionality: replace the content of the Path.ToolTip tags with the following:
The StringFormat option on bindings is new in .NET 3.5 SP1, and using it here gets the display time down from 7.7 seconds to 6.4.

Using Value Converters
Data bindings can optionally reference little classes called value converters, which implement either the IValueConverter or IMultiValueConverter interface. Methods in the value converters named Convert and ConvertBack perform data conversion between the binding source and destination.
Converters are often generalized to be applicable in a variety of applications; one example is the handy BooleanToVisibilityConverter used to convert true and false to Visibility.Visible and Visibility.Collapsed, respectively. But converters can be as ad hoc as you need them to be.
To simplify the DataTemplate and reduce the number of data bindings, two converters could be created. The converter shown in Figure 4 is named IndexToBrushConverter (included in the DataLibrary DLL) and converts a non-negative integer into a Brush. The converter has a public property named Brushes of type Brush array, and the integer is simply an index into that array.
Writing More Efficient ItemsControls Figure 4 The IndexToBrushConverter Class
public class IndexToBrushConverter : IValueConverter {     public Brush[] Brushes { get; set; }      public object Convert(object value, Type targetType,                            object parameter, CultureInfo culture)     {         return Brushes[(int)value];     }      public object ConvertBack(object value, Type targetType,                                object parameter, CultureInfo culture)     {         return null;     } } 

相关文章:

  • 2021-06-20
  • 2022-01-30
  • 2021-12-22
  • 2021-07-11
  • 2021-08-04
  • 2021-08-05
  • 2021-12-03
猜你喜欢
  • 2021-09-01
  • 2022-12-23
  • 2021-10-29
  • 2022-12-23
  • 2022-02-10
  • 2021-06-13
  • 2021-08-26
相关资源
相似解决方案