Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a way of using virtualization with hidden panels or expanders?

I'm trying to improve performance with my WPF application and I'm having problems with a complex ItemsControl. Although I've added Virtualization, there's still a performance problem and I think I've worked out why.

Each item contains a series of expandable areas. So the user sees a summary at the start but can drill down by expanding to see more information. Here's how it looks:

enter image description here

As you can see, there's some nested ItemsControls. So each of the top level items has a bunch of Hidden controls. The virtualization prevents off-screen items from loading, but not the hidden items within the items themselves. As a result, the relatively simple initial layout takes a significant time. Flicking around some of these views, 87% of time is spent parsing and Layout, and it takes a few seconds to load.

I'd much rather have it take 200ms to expand when (if!) the user decides to, rather than 2s to load the page as a whole.

Asking for advice really. I can't think of a nice way of adding the controls using MVVM however. Is there any expander, or visibility based virtualization supported in WPF or would I be creating my own implementation?

The 87% figure comes from the diagnostics:

enter image description here

like image 635
Joe Avatar asked Jun 29 '16 11:06

Joe


3 Answers

If you simply have

- Expander
      Container
          some bindings
    - Expander
          Container
              some bindings
+ Expander
+ Expander
... invisible items

Then yes, Container and all bindings are initialized at the moment when view is displayed (and ItemsControl creates ContentPresenter for visible items).

If you want to virtualize content of Expander when it's collapsed, then you can use data-templating

public ObservableCollection<Item> Items = ... // bind ItemsControl.ItemsSource to this

class Item : INotifyPropertyChanged
{
    bool _isExpanded;
    public bool IsExpanded // bind Expander.IsExpanded to this
    {
        get { return _isExpanded; }
        set
        {
            Data = value ? new SubItem(this) : null;
            OnPropertyChanged(nameof(Data));
        }
    }

    public object Data {get; private set;} // bind item Content to this
}

public SubItem: INotifyPropertyChanged { ... }

I hope there is no need to explain how to to do data-templating of SubItem in xaml.

If you do that then initially Data == null and nothing except Expander is loaded. As soon as it's expanded (by user or programmatically) view will create visuals.

like image 194
Sinatr Avatar answered Oct 23 '22 08:10

Sinatr


I thought I'd put the details of the solution, which is pretty much a direct implementation of Sinatr's answer.

I used a content control, with a very simple data template selector. The template selector simply checks if the content item is null, and chooses between two data templates:

public class VirtualizationNullTemplateSelector : DataTemplateSelector
{
    public DataTemplate NullTemplate { get; set; }
    public DataTemplate Template { get; set; }

    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        if (item == null)
        {
            return NullTemplate;
        }
        else
        {
            return Template;

        }
    }
}

The reason for this is that the ContentControl I used still lays out the data template even if the content is null. So I set these two templates in the xaml:

            <ContentControl Content="{Binding VirtualizedViewModel}"  Grid.Row="1" Grid.ColumnSpan="2" ><!--Visibility="{Binding Expanded}"-->
                <ContentControl.Resources>
                    <DataTemplate x:Key="Template">
                        <StackPanel>
                            ...complex layout that isn't often seen...
                        </StackPanel>
                    </DataTemplate>
                    <DataTemplate x:Key="NullTemplate"/>
                </ContentControl.Resources>
                <ContentControl.ContentTemplateSelector>
                    <Helpers:VirtualizationNullTemplateSelector Template="{StaticResource Template}" NullTemplate="{StaticResource NullTemplate}"/>
                </ContentControl.ContentTemplateSelector>
            </ContentControl>

Finally, rather than using a whole new class for a sub-item, it's pretty simple to create a "VirtualizedViewModel" object in your view model that references "this":

    private bool expanded;
    public bool Expanded
    {
        get { return expanded; }
        set
        {
            if (expanded != value)
            {
                expanded = value;
                NotifyOfPropertyChange(() => VirtualizedViewModel);
                NotifyOfPropertyChange(() => Expanded);
            }
        }
    }


    public MyViewModel VirtualizedViewModel
    {
        get
        {
            if (Expanded)
            {
                return this;
            }
            else
            {
                return null;
            }
        }
    }

I've reduced the 2-3s loading time by about by about 75% and it seems much more reasonable now.

like image 26
Joe Avatar answered Oct 23 '22 06:10

Joe


This simple solution helped me:

<Expander x:Name="exp1">
   <Expander.Header>
     ...
   </Expander.Header>
   <StackPanel
      Margin="10,0,0,0"
      Visibility="{Binding ElementName=exp1, Path=IsExpanded, Converter={StaticResource BooleanToVisibilityConverter}}">
      <Expander x:Name="exp2">
        <Expander.Header>
          ...
        </Expander.Header>
        <StackPanel
          Margin="10,0,0,0"
          Visibility="{Binding ElementName=exp2, Path=IsExpanded, Converter={StaticResource BooleanToVisibilityConverter}}">
like image 38
Alexander Yashyn Avatar answered Oct 23 '22 07:10

Alexander Yashyn