Posts Tagged ‘DataTemplate’

WPF: Using a VirtualizingStackPanel to Improve ComboBox Performance

Monday, December 14th, 2009

Introduction
In this earlier blog, I looked at how to use a ComboBox to display a list of all the system fonts, displaying each font name in its own font style.

I mentioned there that fonts are something of a special case, in that this collection of fonts is automatically cached for you after you first use it. The result is that when you run the application for the second and subsequent times there is minimal delay between the time the user clicks the ComboBox down arrow and the appearance of the list of fonts.

In this blog I want to look at the situation where you have a ComboBox that has a large number of other (possibly graphically complex) items to display.
Without the built-in caching that is available for fonts, you will often find that the display of large amounts of data in a ComboBox can be annoyingly slow. Furthermore, it will continue to be slow each time the ComboBox is recreated (although you can re-access the ComboBox and have shorter delays as long as you don't close the Window). However, if you close the Window that contains this ComboBox, then the whole display has to be rebuilt and the initial longer delay will reoccur.

The reason for the delay is that WPF creates the complete display for the ComboBox in advance,  regardless of the fact that it might only display a tiny proportion of the total number of items. It is this initial unnecessary creation of visual content that causes the problem. Let's create a sample project to demonstrate this.

Display of Large Amount of Text
First, I'll create some fairly uninspiring demo data in the code-behind: 

    Private Sub Window2_Loaded(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles Me.Loaded

        Dim DemoList As New List(Of String)

        For i As Integer = 0 To 10000

            DemoList.Add("This is ItemCollection # " & i.ToString)

        Next

        Me.DataContext = DemoList

    End Sub 

This simply creates 10,000 Strings, stores them in a List (Of String) and sets this List as the DataContext for the Window.

Next, in the markup for the Window, create a ComboBox that will use those Strings for its display of items: 

    <ComboBox Height="23" Margin="27,10,10,0" Name="ComboBox1"

             VerticalAlignment="Top"

             ItemsSource="{Binding}">

    </ComboBox>  

When you run this application, there will be noticeable delay before the list appears. As I mentioned above, when you click for a second and subsequent times, the delay is shorter but still exists. If you close this Window and then subsequently show it again, the long delay will reoccur.

Improving the Display Time
The quick fix for this issue is to insert a VirtualizingStackPanel into the template for the ComboBox. This panel has the ability to assess how many items can be displayed, based on the measurements of the ComboBox, and automatically creates the visuals for that limited number of items only.

An easy way to implement this is to create an ItemsPanelTemplate as a Resource and reference it in the ComboBox markup.  

  <Window.Resources>

    <ItemsPanelTemplate x:Key="VSP">

      <VirtualizingStackPanel/>

    </ItemsPanelTemplate>

  </Window.Resources>

 

 

    <ComboBox Height="23" Margin="27,10,10,0" Name="ComboBox1"

             VerticalAlignment="Top"

             ItemsSource="{Binding}"

             ItemsPanel="{StaticResource VSP}">

    </ComboBox>  

Specifically, the ItemsPanel property of the ComboBox is set to that ItemsPanelTemplate Resource.

If you prefer, you can include the VirtualizingStackPanel right in the ComboBox creation markup: 

   <ComboBox Height="23" Margin="27,10,10,0" Name="ComboBox1"

             VerticalAlignment="Top"

             ItemsSource="{Binding}"

            >

      <ComboBox.ItemsPanel>

        <ItemsPanelTemplate>

          <VirtualizingStackPanel />

        </ItemsPanelTemplate>

      </ComboBox.ItemsPanel>

    </ComboBox>  

Personally, I like to try and keep as much of these kind of things as Resources as I can.

More Complex Items

  If your display is more complex - maybe using a DataTemplate and including nested panels and some images in each item:

    <DataTemplate x:Key="ImgAndText">

      <StackPanel Orientation="Horizontal">

        <Image Source="Timer.jpg" Height="89" Margin="3"></Image>

        <StackPanel>

          <Image Source="Stars.jpg" Width="152" Height="44" Stretch="None"></Image>

          <TextBlock Text="{Binding}" Margin="2,15,2,2" ></TextBlock>

        </StackPanel>

       </StackPanel>

    </DataTemplate>

 

then the delay can become very substantial indeed and you will certainly want to be sure to include a VirtualizingStackPanel in the markup for the ComboBox.

WPF: Using a VirtualizingStackPanel to Improve ComboBox Performance

Monday, December 14th, 2009

Introduction
In this earlier blog, I looked at how to use a ComboBox to display a list of all the system fonts, displaying each font name in its own font style.

I mentioned there that fonts are something of a special case, in that this collection of fonts is automatically cached for you after you first use it. The result is that when you run the application for the second and subsequent times there is minimal delay between the time the user clicks the ComboBox down arrow and the appearance of the list of fonts.

In this blog I want to look at the situation where you have a ComboBox that has a large number of other (possibly graphically complex) items to display.
Without the built-in caching that is available for fonts, you will often find that the display of large amounts of data in a ComboBox can be annoyingly slow. Furthermore, it will continue to be slow each time the ComboBox is recreated (although you can re-access the ComboBox and have shorter delays as long as you don't close the Window). However, if you close the Window that contains this ComboBox, then the whole display has to be rebuilt and the initial longer delay will reoccur.

The reason for the delay is that WPF creates the complete display for the ComboBox in advance,  regardless of the fact that it might only display a tiny proportion of the total number of items. It is this initial unnecessary creation of visual content that causes the problem. Let's create a sample project to demonstrate this.

Display of Large Amount of Text
First, I'll create some fairly uninspiring demo data in the code-behind: 

    Private Sub Window2_Loaded(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles Me.Loaded

        Dim DemoList As New List(Of String)

        For i As Integer = 0 To 10000

            DemoList.Add("This is ItemCollection # " & i.ToString)

        Next

        Me.DataContext = DemoList

    End Sub 

This simply creates 10,000 Strings, stores them in a List (Of String) and sets this List as the DataContext for the Window.

Next, in the markup for the Window, create a ComboBox that will use those Strings for its display of items: 

    <ComboBox Height="23" Margin="27,10,10,0" Name="ComboBox1"

             VerticalAlignment="Top"

             ItemsSource="{Binding}">

    </ComboBox>  

When you run this application, there will be noticeable delay before the list appears. As I mentioned above, when you click for a second and subsequent times, the delay is shorter but still exists. If you close this Window and then subsequently show it again, the long delay will reoccur.

Improving the Display Time
The quick fix for this issue is to insert a VirtualizingStackPanel into the template for the ComboBox. This panel has the ability to assess how many items can be displayed, based on the measurements of the ComboBox, and automatically creates the visuals for that limited number of items only.

An easy way to implement this is to create an ItemsPanelTemplate as a Resource and reference it in the ComboBox markup.  

  <Window.Resources>

    <ItemsPanelTemplate x:Key="VSP">

      <VirtualizingStackPanel/>

    </ItemsPanelTemplate>

  </Window.Resources>

 

 

    <ComboBox Height="23" Margin="27,10,10,0" Name="ComboBox1"

             VerticalAlignment="Top"

             ItemsSource="{Binding}"

             ItemsPanel="{StaticResource VSP}">

    </ComboBox>  

Specifically, the ItemsPanel property of the ComboBox is set to that ItemsPanelTemplate Resource.

If you prefer, you can include the VirtualizingStackPanel right in the ComboBox creation markup: 

   <ComboBox Height="23" Margin="27,10,10,0" Name="ComboBox1"

             VerticalAlignment="Top"

             ItemsSource="{Binding}"

            >

      <ComboBox.ItemsPanel>

        <ItemsPanelTemplate>

          <VirtualizingStackPanel />

        </ItemsPanelTemplate>

      </ComboBox.ItemsPanel>

    </ComboBox>  

Personally, I like to try and keep as much of these kind of things as Resources as I can.

More Complex Items

  If your display is more complex - maybe using a DataTemplate and including nested panels and some images in each item:

    <DataTemplate x:Key="ImgAndText">

      <StackPanel Orientation="Horizontal">

        <Image Source="Timer.jpg" Height="89" Margin="3"></Image>

        <StackPanel>

          <Image Source="Stars.jpg" Width="152" Height="44" Stretch="None"></Image>

          <TextBlock Text="{Binding}" Margin="2,15,2,2" ></TextBlock>

        </StackPanel>

       </StackPanel>

    </DataTemplate>

 

then the delay can become very substantial indeed and you will certainly want to be sure to include a VirtualizingStackPanel in the markup for the ComboBox.

WPF: Displaying a Master-Detail Collection of In-Memory Objects In a TreeView

Tuesday, November 17th, 2009

 Introduction
In this earlier blog I used an XML file containing nested data items as the data source of a TreeView. In this version I will use a collection of objects as the data source.
The details of the data are much the same as I used in the previous blog, with a couple of tiny changes to avoid any possible confusion caused by the use of Visual Basic keywords as field names:

These classes are saved in a VB file named SalesData.vb. Because it is fairly lengthy, filling about three screen display lengths, I've made it available from this link. (To a large extent, how the the data is created isn't really the topic of this blog - I'm more concerned with the Binding to a source and the templates used for displaying the data.)

Mapping to the local Assembly
In order to be able to access those SalesPerson, SalesOrder, etc classes and objects in the XAML file, it is necessary to create a mapping. The syntax for this is as follows:  

  xmlns:local="clr-namespace:HierarchicalDataTemplate" 

It isn't mandatory to use 'local' as the mapping alias, but it's a fairly traditional approach. In this case, of course 'HierarchicalDataTemplate' is the name of the project I am working on.

With the namespace mapping in place, any of the classes that currently exist in the code-behind files of the project become visible to the XAML file. If you view the code listing for the SalesData.vb file, you will see that the class which creates the demo data is called 'SalesPersonList'. It's now possible to create a new instance of that class in the XAML file : 

    <local:SalesPersonList x:Key="SalesPersonList"/> 

I generally place this in the Window.Resources collection.

Creating a TreeView and Binding its Data Source
In the markup for the Window itself, I'll create a TreeView which contains a single TreeViewItem.  

    <TreeView>

      <TreeViewItem ItemsSource="{Binding Source={StaticResource SalesPersonList}}"

          Header="Sales Figures" />

    </TreeView> 

The ItemsSource is the crucial property here. It identifies exactly where the TreeView should look for its data. In this case it looks into the SalesPersonList instance that I created a few moments ago. For the avoidance of doubt, the exact 'SalesPersonList' that is used is the StaticResource created earlier and identified by the Key. (I possibly should have used a different name for the Key and the underlying Class to avoid any confusion).

Because of the use of the Binding and the HierarchicalDataTemplates, it is only necessary to create this single TreeViewItem. The Binding engine will trawl through all the data and create as many TreeViewItem nodes as it needs to in order to display everything correctly. 

HierarchicalDataTemplates
This example uses three HierarchicalDataTemplates, plus a standard DataTemplate for the SalesItems. Here is the first one (which again I've placed in the Window.Resources collection):  

    <HierarchicalDataTemplate DataType="{x:Type local:SalesPerson}"

          ItemsSource="{Binding Path=Periods}">

      <TextBlock Text="{Binding Path=Name}"/>

    </HierarchicalDataTemplate> 

The DataType property identifies which type of object will be dealt with by this template. In the case of this first template, this will be the SalesPerson type. In other words, when the Binding that I placed on the TreeViewItem finds any instance of a SalesPerson inside that SalesPersonList, it will look at this template to discover how it should display SalesPerson details.
In this case, the Name property of the current SalesPerson will be shown in a TextBlock.
The ItemsSource property also uses a Binding, but it is only interested in knowing what needs to be shown as the child data of SalesPersons. As we know from the diagram above, this will be the Periods data.

If you are finding the DataType and the Bindings' Paths a bit tricky to grasp, it may help if you run the project as it currently stands. Here's the markup for the Window so far: 

<Window x:Class="OrdersListsDisplay"

   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

   xmlns:local="clr-namespace:HierarchicalDataTemplate"

   Title="Sales List Display" Height="300" Width="300">

  <Window.Resources>

    <local:SalesPersonList x:Key="SalesPersonList"/>

 

    <!--  Data Templates -->

    <HierarchicalDataTemplate DataType="{x:Type local:SalesPerson}"

          ItemsSource="{Binding Path=Periods}">

      <TextBlock Text="{Binding Path=Name}"/>

    </HierarchicalDataTemplate>

 

  </Window.Resources>

    <Grid>

    <TreeView>

      <TreeViewItem ItemsSource="{Binding Source={StaticResource SalesPersonList}}"

          Header="Sales Figures" />

    </TreeView>

  </Grid>

</Window> 

When you run this, you will first see:

Then, when you expand the first node, you will have:

Finally, clicking on either or both the SalesPerson nodes, you will see that the application has tried to display the children of the SalesPersons for you. In the absence of any instruction about formatting the Periods, it simply reverts to displaying the default ToString rendering of the class.

Hopefully though you can now see why the template points to SalesPerson as its DataType, but identifies the next level down the tree as the ItemsSource. (You may have noticed that this wasn't necessary with the XML data example in the earlier blog, where simply assigning a Binding without a Path will work.)

Displaying Periods and SalesOrders
Exactly the same approach is used for the next two templates: 

    <HierarchicalDataTemplate DataType="{x:Type local:Period}"

          ItemsSource="{Binding Path=SalesOrders}">

      <TextBlock Text="{Binding Path=Name}"/>

    </HierarchicalDataTemplate>

 

    <HierarchicalDataTemplate DataType="{x:Type local:SalesOrder}"

         ItemsSource="{Binding Path=SalesItems}">

      <TextBlock Text="{Binding Path=Name}"/>

    </HierarchicalDataTemplate> 

The three HierarchicalDataTemplates in place so far will produce this result:

which should come as no surprise, based on what I explained earlier.

ItemDetail DataTemplate
So that just leaves the template for the SalesItems and ItemDetail data. This time a standard DataTemplate will do, because we know there is no further data below ItemDetail.  

    <DataTemplate DataType="{x:Type local:SalesItem}">

      <TextBlock Text="{Binding Path=ItemDetail}"/>

    </DataTemplate> 

Just for the record, a HierarchicalDataTemplate would work but isn't necessary. With the final DataTemplate added, every level of the Master-Detail data can be accessed:

Formatting
You have many options for formatting the final presentation of the data. In this example, you can change the color, font size and weight, indentation, etc via the individual TextBlock properties. If you wanted to insert additional graphical detail (such as an icon), you simply wrap the TextBlock in a StackPanel with its Orientation set to Horizontal. You can then insert the icon image as an additional child of this StackPanel.

You can build on this basic example shown here to create a much more complex display.

WPF ListView – Formatting Rows

Monday, September 7th, 2009

 By the end of the previous blog item on the topic of the WPF ListView, the ListView looked like this:

In this blog I want to look at ways of changing the look of the rows. You may, for example, want to assign a background color to the rows. And based on what we have done so far, you might be tempted to edit the DataTemplates used for the cells. Maybe you would think about adding a Border with a Background fill and placing this around the TextBlock for each cell:  

  <DataTemplate x:Key="IDCellTemplate2">

      <Border Background="LightGreen">

      <TextBlock Foreground="MediumBlue"

                FontFamily="Calibri"

         Text="{Binding Path=ProductID}" />

      </Border>

    </DataTemplate> 

I won't waste the space showing the markup for all three templates here, but if you did try this you will get a disappointing result:

Pretty ugly, I think you'll agree. Playing around with the Margin or Padding won't get you any further either.

The answer is to use the ItemContainerStyle property of the ListView. This opens up a number of choices for you. Firstly, if you would like to still have a gap between each of the columns - but an even gap - then you can take the following approach:  

      <ListView.ItemContainerStyle>

        <Style TargetType="ListViewItem">

          <Setter Property="HorizontalContentAlignment" Value="Stretch" />

        </Style>

      </ListView.ItemContainerStyle> 

This will produce the following output, which is an improvement:

If you want the light green to spread across the whole row, then you still use the ItemContainerStyle, but this time you set the Background property of the ListViewItem:  

      <ListView.ItemContainerStyle>

        <Style TargetType="ListViewItem">

            <Setter Property="Background" Value="LightGreen" />

        </Style>

      </ListView.ItemContainerStyle> 

Now you get the result you are probably looking for.

 

If all that whiteness in the background is a bit too much for you, then of course you can blend another shade of green into the mix by setting the ListView's Background property. Using SeaGreen, for example, will produce this:

 

 

The final markup for the ListView looks like this:  

    <ListView Name="ProductsListView"

         ItemsSource="{Binding}"

           Margin="5,25"

              Background="SeaGreen">

      <ListView.ItemContainerStyle>

        <Style TargetType="ListViewItem">

            <Setter Property="Background" Value="LightGreen" />

        </Style>

      </ListView.ItemContainerStyle>

 

      <ListView.View>

        <GridView>

          <!-- Product ID -->

          <GridViewColumn

           HeaderTemplate="{StaticResource IDColHeader}"

          CellTemplate="{StaticResource IDCellTemplate}">

          </GridViewColumn>

          <!-- Product Name -->

          <GridViewColumn

           HeaderTemplate="{StaticResource NameColHeader}"

           CellTemplate="{StaticResource NameCellTemplate}">

          </GridViewColumn>

          <!-- Pack Size -->

          <GridViewColumn

           HeaderTemplate="{StaticResource PackageColHeader}"

           CellTemplate="{StaticResource PackCellTemplate}">

          </GridViewColumn>

        </GridView>

      </ListView.View>

    </ListView> 

The markup for the DataTemplates and Styles is the same as that shown in the previous blog, all of which I placed in the Application.xaml file (App.xaml for C# projects).

There are some other row formatting options I want to cover, but I will leave those for another day.