Sep 22, 2007

How to improve TreeView’s performance – Part III

41TreeViewPerformancePart3

Update: This post is out of date. With .NET 3.5 SP1, TreeView now provides opt-in UI virtualization. You can see this feature working by setting VirtualizingStackPanel.IsVirtualizing=”True” on the TreeView itself. TreeView also supports container recycling, which you can control by setting the VirtualizingStackPanel.VirtualizationMode property.


In part I of my discussions on TreeView performance, I presented the three main limitations in the current implementation of TreeView that may lead to slower performance:

  • UI elements stay in memory even when collapsed.
  • There is no UI virtualization.
  • There is no data virtualization.

In part II, I talked about a solution where I introduced a middle tier between the UI and the data layer, that discards the data when a TreeViewItem is collapsed, and brings the data back into memory when a TreeViewItem is expanded. This solution completely fixes the first limitation of TreeView – the UI elements no longer stay in memory after expanding and collapsing a TreeViewItem. It also partially fixes the lack of data virtualization in TreeView because we only keep a small portion of the data in memory. I say “partially” because it virtualizes data on expand/collapse, but it does not take scrolling into account.

Today I will discuss a solution that builds on top of the previous one by providing UI virtualization.

With the current version of WPF, only two Controls offer UI virtualization: ListBox and ListView (actually, ListView gets it for free because it derives from ListBox). The work to make virtualization happen is done by VirtualizingStackPanel, which is the panel used by default in ListBox. It would be nice if we could simply tell TreeView to use a VirtualizingStackPanel to lay out its items, but unfortunately it’s not that simple. VirtualizingStackPanel understands only flat lists of items, so it is not capable of laying out the hierarchical data required for a TreeView.

On the other hand, styles and templates are among the most powerful features of WPF because they allow you to completely change the look of a control while retaining its behavior. For example, this post shows how a ListBox can easily be customized to look like a diagram of our solar system. With this in mind, Ben Carter (an awesome dev on the WPF team) had the brilliant idea of simply making a ListBox look like a TreeView. This allows us to use VirtualizingStackPanel for free, which offers UI virtualization. And you will see how easy it is to make a ListBox look like a TreeView, thanks to the power of styles and templates in WPF. We’ll need to make a few changes to the data side, but I will explain what they are.

I started by thinking about the theming portion of this scenario. To make my ListBox look like a TreeView, I need the toggle button that expands and collapses TreeViewItems. I used Blend, once again, to dig into the default style for the ToggleButton in TreeViewItem (which in the Aero theme looks like a triangle), and copied it to the window’s resources. This style contains triggers to change its look when the mouse is over it, and to rotate it when a user clicks on it. Then I added the following DataTemplate to the ItemTemplate property of my ListBox:

    …
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <ToggleButton x:Name="tb" ClickMode="Press" Style="{StaticResource ExpandCollapseToggleStyle}"/>
                <TextBlock Text="{Binding Path=ShortName}" />
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
    …

I tested it with a rudimentary data source – a simple (non-hierarchical) ObservableCollection of RegistryKey items that contain a ShortName property. This helped me understand how I need my data to be presented to the ListBox.

Adding and removing items in the ObservableCollection

My first realization was that my data source can not be hierarchical this time, because ListBox only understands flat lists of data. So I will need to have a single ObservableCollection with the data for all the visible items, regardless of their depth in the original hierarchy. I will have to update this list of visible items any time the user expands or collapses an item. When the user expands an item, I will insert the item’s children just after it in the ObservableCollection. When the user collapses an item, I will remove its children from the ObservableCollection. Here is the code I wrote to make this happen:

    public class RegistryData3 : INotifyPropertyChanged
    {
        private ObservableCollection<RegistryKeyHolder3> allKeys;
        …

        public ObservableCollection<RegistryKeyHolder3> AllKeys
        {
            get { return allKeys; }
        }

        …
        public RegistryData3()
        {
            this.allKeys = new ObservableCollection<RegistryKeyHolder3>();
            this.AddNewKeyHolder(Registry.CurrentUser);
            this.AddNewKeyHolder(Registry.CurrentConfig);
            …
        }

        private void AddNewKeyHolder(RegistryKey registryKey)
        {
            RegistryKeyHolder3 newKeyHolder = new RegistryKeyHolder3(registryKey, 0);
            newKeyHolder.PropertyChanged += new PropertyChangedEventHandler(KeyHolder_PropertyChanged);
            this.allKeys.Add(newKeyHolder);
        }

        public void PopulateSubKeys(RegistryKeyHolder3 parentKeyHolder)
        {
            int indexParentKey = this.allKeys.IndexOf(parentKeyHolder);
            if (indexParentKey == this.allKeys.Count – 1 || this.allKeys[indexParentKey + 1].Level <= parentKeyHolder.Level)
            {
                string[] subKeyNames = parentKeyHolder.Key.GetSubKeyNames();
                for (int i = 0; i < subKeyNames.Length; i++)
                {
                    RegistryKeyHolder3 childKeyHolder = new RegistryKeyHolder3(parentKeyHolder.Key.OpenSubKey(subKeyNames[i]), parentKeyHolder.Level + 1);
                    childKeyHolder.PropertyChanged += new PropertyChangedEventHandler(KeyHolder_PropertyChanged);
                    allKeys.Insert(indexParentKey + i + 1, childKeyHolder);
                    …
                }
            }
        }
        …

        public void ClearSubKeys(RegistryKeyHolder3 parentKeyHolder)
        {
            int indexToRemove = this.allKeys.IndexOf(parentKeyHolder) + 1;
            while ((indexToRemove < this.allKeys.Count) && (this.allKeys[indexToRemove].Level > parentKeyHolder.Level))
            {
                this.allKeys.RemoveAt(indexToRemove);
                …
            }
        }

        …
    }

The PopulateSubKeys method is responsible for adding an item’s children to the ObservableCollection when the user expands that item. This method retrieves the children of the parent item, creates a RegistryKeyHolder3 instance for each item and inserts those instances starting at the index just after the parent. Don’t worry about the Level concept you see in this code; I will explain it in the next section. I will also explain and show the code for the property changed event handler later in this post.

The ClearSubKeys method removes an item’s children from the list, and is called when the user collapses the parent. It starts removing items from the list in the index after the parent’s and continues until the expected number of items has been removed.

These two methods allow us to keep a flat list with the items in the order we want the ListBox to display them. Adding items to and removing items from the flat list achieves partial data virtualization, just like the solution in my previous post.

Indentation

I also realized that I need to tag each RegistryKeyHolder3 data item with the level they belong to, which will help me figure out how much they have to be indented in the ListBox. I decided to add a property called “Level” to the RegistryKeyHolder3 class for that purpose. For the root keys the Level property will be set to 0, for the next level it will be set to 1, and so on. Notice that while constructing the children key holders, the code in PopulateSubKeys specifies that the level of the children is the parent’s level incremented by 1. Also, in the ClearSubKeys method, one of the conditions to stop removing children is encountering an item that has the same level as the one being collapsed.

To indent the items in the UI based on the Level value, I added a Border to the left of the expander and text and bound its Width property to the Level property in the source:

    …
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <Border Width="{Binding Path=Level, Converter={StaticResource ConvertLevelToIndent}}" />
                <ToggleButton x:Name="tb" ClickMode="Press" … Style="{StaticResource ExpandCollapseToggleStyle}"/>
                <TextBlock Text="{Binding Path=ShortName}" />
            </StackPanel>
            …
        </DataTemplate>
    </ListBox.ItemTemplate>
    …

In order to convert a Level value to the Border’s Width, I defined the following converter:

    public class ConvertLevelToIndent : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return (int)value * 16;
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotSupportedException("Not supported – ConvertBack should never be called in a OneWay Binding.");
        }
    }

Parent item expansion

I decided to add an “IsExpanded” property to the RegistryKeyHolder3 class that will help me tie the children expansion on the data side with the visual rotation of the toggle button in the UI.

    …
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <Border Width="{Binding Path=Level, Converter={StaticResource ConvertLevelToIndent}}" />
                <ToggleButton x:Name="tb" ClickMode="Press" IsChecked="{Binding Path=IsExpanded}" Style="{StaticResource ExpandCollapseToggleStyle}"/>
                <TextBlock Text="{Binding Path=ShortName}" />
            </StackPanel>
            …
        </DataTemplate>
    </ListBox.ItemTemplate>
    …

If you take a look at the ToggleButton’s XAML, you will notice that its IsChecked property is bound to the IsExpanded property. The Mode of this binding is TwoWay – no Mode is defined explicitly, but I know that’s the default Mode for the IsChecked DP.

Also, if you look at the code that adds items to the list in the PopulateSubKeys method, you will notice that I added KeyHolder_PropertyChanged as the handler for the PropertyChanged event on RegistryKeyHolder3. Here is the code for that event handler:

    void KeyHolder_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == "IsExpanded")
        {
            RegistryKeyHolder3 keyHolder = (RegistryKeyHolder3)sender;
            if (keyHolder.IsExpanded)
            {
                this.PopulateSubKeys(keyHolder);
            }
            else
            {
                this.ClearSubKeys(keyHolder);
            }
        }
    }

When the application starts, all the items appear collapsed because the IsExpanded property of each data item is initialized to false, and the IsChecked property is bound to IsExpanded. Here is what happens when the user expands an item:

1) When the user clicks on the ToggleButton to expand an item, IsChecked becomes true, and because of the TwoWay binding, the IsExpanded property for the corresponding data item is set to true.

2) RegistryKeyHolder3 raises a PropertyChanged event when its IsExpanded property changes, causing the code in the handler (the KeyHolder_PropertyChanged method in RegistryData3) to be executed.

3) Because IsExpanded is true for this data item, the PopulateSubKeys method on RegistryData3 is called, causing the children of this item to be added to the list and displayed in the ListBox.

You can imagine a similar sequence of events when the user clicks to collapse an item.

Visibility of the expander

Lastly, I wanted to make the expander for a particular item hidden whenever that item has no children. I was able to do this by adding a simple DataTrigger that causes the ToggleButton to be hidden whenever the Key’s SubKeyCount property is zero, and visible otherwise. You can see the complete XAML for the ItemTemplate’s DataTemplate here:

    …
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <Border Width="{Binding Path=Level, Converter={StaticResource ConvertLevelToIndent}}" />
                <ToggleButton x:Name="tb" ClickMode="Press" IsChecked="{Binding Path=IsExpanded}" Style="{StaticResource ExpandCollapseToggleStyle}"/>
                <TextBlock Text="{Binding Path=ShortName}" />
            </StackPanel>
            <DataTemplate.Triggers>
                <DataTrigger Binding="{Binding Path=Key.SubKeyCount}" Value="0">
                    <Setter Property="Visibility" TargetName="tb" Value="Hidden"/>
                </DataTrigger>
            </DataTemplate.Triggers>
        </DataTemplate>
    </ListBox.ItemTemplate>
    …

Conclusion style='font-weight:bold'>

This solution provides true UI virtualization, as you can see in the screenshot below, where I expanded the three first items (in depth first order). If you scroll the third TreeView (or should I say ListBox?), you will see that for a little while the number of UI elements in memory increases, but it quickly settles on a number much lower than the other two TreeViews. This delay happens because we queue in the dispatcher the operation to clean up those items with a low priority so that it doesn’t make the UI unresponsive.

And just like the solution in my previous post, this solution discards children elements on collapse and provides a partial data virtualization solution.

So, should you all switch your TreeViews to ListBoxes? Well, as with almost everything in life, there is a price to pay for the benefits of this solution: the programming model is more cumbersome than if you were using a TreeView. You will not be able to use HierarchicalDataTemplates to style your items, you’ll miss the convenience properties and methods of TreeView, you’ll have to introduce a slightly complex intermediate layer between your UI and your data, and you will have to work hard to minimize the inconsistencies in the UI. In short, you can make a ListBox look like a TreeView, but you can’t make a ListBox become a TreeView.

Whether this solution is right for you depends on how much you value the performance gain over the disadvantages it brings.

Here you can find the project with this code built using VS 2008 RTM.

31 Comments
  1. Will

    Probably asked before, but is there a way to do something similar with a wrap panel? If so, which would be easier:

    a) virtualize a wrap panel
    b) wrap a stack panel

    I found an article on Dan Crevier’s blog about a tile panel, but it was extremely outdated and based on beta wpf. Specifically, I’m trying to load picture thumbnails (several thousand) into a scrollable wrap panel. Will there be one in 3.5 of the framework?

    Thanks,

    Will

    • Will

      Nevermind the last comment. A little bit of work and I was able to make Dan’s TilePanel work. A bit of a “poor man’s” Virtualized WrapPanel, but it works. Would still love to see a Virtualized WrapPanel that works as well as the Virtualized Stack Panel does in a future release of the framework.

      Thanks,

      Will

      • Bea

        Hi Will,

        Yes, we hear that feature request once in a while. I made sure that we have that tracked. I’m glad you were able to get something working in the meantime.

        Thanks,
        Bea

  2. aelij

    Very useful! The ListView solution also has two other advantages: you get multiple selection (which is not available in TreeView) and you can use GridView to get columns in your tree.

    • Bea

      Hi Aelij,

      Yes, those are good observations. Thanks for pointing them out.

      Bea

  3. Martin

    Wow! This was a GREAT series. This is EXACTLY what I’ve been wanting to do with the Tree View but didn’t know how with WPF. In Win32 I’d just override the WM_PAINT message and implement it from scratch, and this truly speaks volumes to the flexibility of the WPF system.

    Thanks so much for such a great series!!

    • Bea

      Hi Martin,

      I’m glad you enjoyed the topic :)

      Thanks for reading my blog!
      Bea

  4. Marlon

    Hi there,

    I like the idea, yet I would still prefare if the VirtualizingStackPanel understood hierarchies… If I may ask, why is this not implemented in the VirtualizingStackPanel?

    Again, nice idea and keep up the good work… :)

    Regards
    Marlon

    • Bea

      Hi Marlon,

      Yes, the solution in my blog is basically a workaround for the fact that there is no way to do UI virtualization for a hierarchy of items, in WPF. Ideally, we would have a panel that virtualizes hierarchies (either VirtualizingStackPanel or something else).

      My team has been thinking a lot about the problems and possible solutions in this area lately, so hopefully you’ll see some improvoments in the next version.

      Bea

        • Bea

          Hi Marlon,
          :) Thanks a lot :)
          I’ll be interested to see what you come up with, in regards to VirtualizingStackPanel.

          Bea

        • Marlon

          Hi Bea,

          How are you? hope you are ok… I implemented my first Virtualizing Tree view… It is not a pure Virtualizing Tree View it is a ListBox that understands hierarchies and basically it flattens them so that it can use Virtualization…

          Have a look at it if you have some time
          http://marlongrech.wordpress.com/2007/09/27/virtualizing-treeview-aka-treelistbox/

          If you can please send me your feed back… even an email (it would be an honour for my e mail account to receive a mail from you)

          Best Regards

        • Bea

          Hi Marlon,

          It’s a great solution you came up with! I have a question for you: will it work with multiple levels of hierarchy?

          The WPF team is thinking of a slightly different approach to provide virtualization to TreeView. The same way the virtualizing behavior of ListBox is encapsulated in the VirtualizingStackPanel class (the ListBox itself has no knowledge of virtualization), we are hoping that TreeView will have no virtualization code in it. But of course, this requires a lot of work, which is why people like you and me come up with simplified solutions that come close to the behavior people want today :)

          Thanks a lot for thinking of these problems! I find them extremely interesting, and I’m happy there are others in the world who do too.

          Bea

        • Marlon Grech

          Yes this solution supports multiple levels. As such what I did is simulate the tree view for a listbox….

          This also supports data binding on all levels…

          Once you have a look at the actual source code you will understand better what I mean…

          Regarding the Hierarchal VirtualizingStackPanel, this is a feature that is really complex to implement. Yet having this feature would be much better than my current solution…
          On the other hand the solution that I am using right now is flexible and performant enough for me so I will stick with this for now :)

          Best regards

        • Marlon

          Hi there,

          me again… this weekend I am planning to re write the control because I found a better way which would use more the virtualizationstackpanel features…

          this is still experimental but I will keep you informed…

          regards

        • Marlon Grech

          Hi Bea,

          How are you? hope you had a nice weekend :)

          As promised I re-wrote the TreeListBox with the new idea that I had… I was really amazed to see, how much better the performace (+ memory) is with the new implementation(compored with the old implementation)…. have a look at it and give me your feedback if you have 10 minutes free…
          http://marlongrech.wordpress.com/2007/10/01/virtualizing-treeview-aka-treelistbox-v20/

          With this new version I am using more the VirtualizationStackPanel and beleive me the difference is really good… I think who ever created the VirtualizingStackPanel did a great job!!!

          Any bugs or suggestions please send an email at marlongrech@gmail.com

          Thanks a lot :)
          Hope to hear from you soon

          Regards
          Marlon

        • Bea

          Hi Marlon,

          That’s awesome! I didn’t download the code, but by reading the post it sounds great!
          I’m glad you’re thinking about these problems, as good solution greatly benefit the WPF community!

          Thanks a lot!
          Bea

  5. Alex Black

    Hi Beatriz, I came across this treeview sample:

    http://blogs.msdn.com/atc_avalon_team/archive/2006/03/01/541206.aspx

    And I love it, its very powerful, but the treeview won’t scroll! Any ideas? or do you know how i could contact the person who wrote it?

    Basically if you download that sample, open it up in Orcas Beta2, run it, expand all the nodes, then srhink the window so that the items don’t fit vertically, you’d expect it to scroll but it doesn’t!

    Any ideas? If I switch it out to a regular Treeview then it does scroll :(

    thx

    - Alex

    • Bea

      Hi Alex,

      You can get the TreeListView to scroll by adding a Scrollviewer to its ControlTemplate, surrounding everything else. Here’s the XAML for this:

      <Style TargetType=”{x:Type l:TreeListView}”>

      <ControlTemplate TargetType=”{x:Type l:TreeListView}”>
      <ScrollViewer>
      <Border BorderBrush=”{TemplateBinding BorderBrush}” BorderThickness=”{TemplateBinding BorderThickness}”>

      </Border>
      </ScrollViewer>

      </Style>

      I uploaded to my server a version of the TreeListView that included this change – you can find it here.

      Let me know if this is what you were looking for.

      Bea

      • Alex Black

        Hi Beatriz, related to my last question, could you show how to extend your example in this blog post to have columns? Perhaps by making a Listview look like a Treeview instead of a Listbox look like a Treeview?

        - Alex

        • Bea

          Hi Alex,

          Changing this blog’s sample to use a ListView is quite simple. This results in basically the same UI as the ATC sample you mentioned in your earlier post, but with UI virtualization.

          You can find here this blog’s sample modified to use a ListView.

          Thanks,
          Bea

  6. Serene

    Hi Bea,

    I have a binding issue which I need to get resolved as soon as possible. It’s not really related to this article tho.. sorry to be posting this here, but you are like my Binding Guru.

    Anyway, I have an object (ObjectA) which has another object in it (ObjectB)

    public class ObjectA
    {
    int ID
    object ObjectB
    }

    public class ObjectB
    {
    int ID
    string Name
    }

    Now, I have to bind a Form to ObjectA, but there is a field which has to be binded to the Name property of ObjectB. How should I do this?

    Serene

    • Bea

      Hi Serene,

      We support data binding to sub-properties by using dot notation, like in ObjectName.SubProperty. So, for your scenario, you could do the following:

      <TextBlock Text=”{Binding Path=ObjectB.Name}” />

      You can find a solution with the necessary code to make this work here.

      Let me know if this helps.

      Bea

  7. Adolfo Wiernik

    Hi Beatriz, I love binding in WPF and I follow your blog very closely… thanks for all the great tips and info!

    I have a quick one…. is it possible to bind against a Setter’s value in a Style?

    Say I have a Textbox with a Style set, but the in the Text property I want to show a value coming from the Style, for example the FontSize….. any tips?

    Thanks!

    • Bea

      Hi Adolfo,

      Yes, that is supported. Here is the xaml for that:

      <Window.Resources>
      <Style x:Key=”SpStyle” TargetType=”{x:Type TextBox}”>
      <Setter Property=”FontSize” Value=”16″ />
      <Setter Property=”Text” Value=”{Binding RelativeSource={RelativeSource Self}, Path=FontSize}”/>
      </Style>
      </Window.Resources>

      <StackPanel>
      <TextBox Style=”{StaticResource SpStyle}”/>
      </StackPanel>

      Thanks,
      Bea

  8. Sean

    Hi, Bea;

    It’s a question related to the ListBox’s Virtualization.
    As you know, the ScrollViewer.CanContentScroll has to be set true to ensure the Virtualization is On, right? But this comes to a problem, only logical scrolling could works but not pixel-based. When the ListBoxItems’ height are not always the same, the length of the vertical scrollbar thumb’s behavior becomes longer and shorter when you scroll the listbox. I’m wondering if there’s a way to make the physical scrolling when the Virtualization is On.

    Thanks.
    Sean

    • Bea

      Hi Sean,

      Unfortunately there is no easy way to work around that limitation. The only way would be for you to re-implement significant portions of the current virtualization code. And if you decide to re-implement portions of our code to allow virtualization with pixel-based scrolling, you will have to solve an interesting problem: How do you determine the size of the thumb when you only know the height of the items visible in the ListBox? Remember that with UI virtualization you won’t have access to the height of most ListBoxItems.

      Bea

      • Sean

        Yes, you’re right. I’m considering the interesting question.
        But let’s put aside the Virtualization&Pixel-Scrolling problem, and make the scenario simpler first, it seems the ListBox’s thumb length will not be correct unless I set ScrollViewer.CanContentScroll=”False”, while I’ve changed the ItemsPanel of the ListBox with StackPanel, it’s nothing about Virtualization.

        Code Snippet:

        <ListBox ScrollViewer.CanContentScroll=”True”>
        <Button Height=”50″>Button 1</Button>
        <Button Height=”150″>Button 2</Button>
        <Button Height=”300″>Button 3</Button>
        <Button Height=”50″>Button 4</Button>
        <Button Height=”50″>Button 5</Butto>
        <ListBox.ItemsPanel>
        <ItemsPanelTemplate>
        <StackPanel />
        </ItemsPanelTemplate>
        </ListBox.ItemsPanel>
        </ListBox>

        Here’s the question, is it possible to make the thumb length correct with no consideration about the Virtualization?

        Regards,
        Sean

        • Bea

          Hi Sean,

          For item-based scrolling, the thumb size is calculated based on the total amount of items and the number of displayed items. If the items have different sizes, it may happen that the number of items displayed differs as you scroll. This causes the size of the thumb to change too.

          You may be able to retemplate the ScrollBar to override the height of the thumb, and then do the calculations yourself. I haven’t tried this, it’s just the first idea that comes to mind. If you’re able to implement this idea successfully, send me email.

          Bea

          • Nick

            I’m not sure I understand why the thumb size is an issue. If the thumb size remains constant, why not simply have a scroll speed that is not uniform? For example, much like item based scrolling, I calculate marks along the scrollbar that represent each item, regardless of how large that item actually is. When scrolling in the view however, I could simply move at a constant rate, say, 30pixels when using the scroll wheel, then when the user is dragging the scrollbar and moving the scrollbar through a section dedicated to say item #30, which when we render we find out is quite large, we just change the rate at which we are moving the viewport content compared to the pixels the scrollbar is moved.

            So, say we have 10 items that just happen to be 30px high each, except for #5 which is 90px high. Our Scrollbar has 10 sections representing the start and end of each item (logically, because we don’t know the height yet). Each time the scrollwheel is moved, we move 30px, and we discover a new item, it’s realized and a UI component is generated. We keep going, the scrollbar is moving along one section at a time with each click of our scrollwheel.

            When we reach 5, we generate the UI and find out the item is actually 90px high. Now, I scroll again with the scrollwheel another 30px go by, but the scrollbar, instead of moving to the next logical tick is computing how far to move based on distance through the current item, so it only moves 1/3 of the way through item 5′s position on the scrollbar.

            So basically it’s like having different densities along the scrollbar.

            Now…If i knew where to modify the code to make this happen, that would be awesome :)

          • Bea

            That is certainly one solution to the problem – there are others.
            As with almost everything in WPF, it’s a compromise.
            Thanks for posting!

Comments are closed.