Search code examples
wpfvirtualizationitemscontrol

Virtualize ItemsControl with a height of * inside an ItemsControl


I have ran into a virtualization problem recently and I have narrowed it down to the following code.

The reason why virtualization doesn't work in the following snippet is due to the fact that the child has no specific height. So my guess is that it expands forever and virtualization breaks.

Giving the child a specific height fixes the problem but then the interface becomes two unsightly scroll bars when I would like one scroll bar, to scroll through the whole content generated by the items controls (if its child, or not).

My question is, is this possible? if so how can I achieve this? Somehow the child needs to calculate the size of itself without breaking virtualization. It seems setting a height of * does not work.

MainWindow.xaml

<Window x:Class="WpfItemsControlVirtualization.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="500" >
<Window.Resources>
    <ResourceDictionary>
        <!--Virtualised ItemsControl-->
        <Style x:Key="ItemsControlVirtialisedStyle" TargetType="ItemsControl">
            <Setter Property="VirtualizingStackPanel.IsVirtualizing" Value="True"/>
            <Setter Property="ScrollViewer.CanContentScroll" Value="True"/>
            <Setter Property="ItemsPanel">
                <Setter.Value>
                    <ItemsPanelTemplate>
                        <VirtualizingStackPanel />
                    </ItemsPanelTemplate>
                </Setter.Value>
            </Setter>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="ItemsControl">
                        <ScrollViewer Padding="{TemplateBinding Control.Padding}" Focusable="False">
                            <ItemsPresenter SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />
                        </ScrollViewer>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </ResourceDictionary>
</Window.Resources>
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"></RowDefinition>
        <RowDefinition Height="Auto"></RowDefinition>
        <RowDefinition Height="*"></RowDefinition>
    </Grid.RowDefinitions>
    <Button Grid.Row="0" Content="Go" Click="ButtonBase_OnClick"/>
    <Button Grid.Row="1" Content="Expand" Click="ButtonBase_OnClick2"/>
    <Expander Grid.Row="2" >
        <ItemsControl ItemsSource="{Binding Collection}" Style="{StaticResource ItemsControlVirtialisedStyle}" VirtualizingPanel.ScrollUnit="Pixel">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Grid>
                        <Grid.RowDefinitions>
                            <!-- <RowDefinition Height="*"></RowDefinition> --> <!-- VIRTUALIZATION BREAK -->
                            <RowDefinition Height="500"></RowDefinition>
                        </Grid.RowDefinitions>
                        <ItemsControl ItemsSource="{Binding Collection}" Style="{StaticResource ItemsControlVirtialisedStyle}" VirtualizingPanel.ScrollUnit="Pixel">
                                <ItemsControl.ItemTemplate>
                            <DataTemplate>
                                <TextBox Text="{Binding Test}" />
                                    </DataTemplate>
                                </ItemsControl.ItemTemplate>
                            </ItemsControl>
                    </Grid>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </Expander>
</Grid>

MainWindow.xaml.cs

    using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;

namespace WpfItemsControlVirtualization
{
    /// <summary>
    /// Implements the INotifyPropertyChanged interface for data binding purposes. 
    /// </summary>
    public abstract class ViewModelBase : INotifyPropertyChanged, INotifyPropertyChanging
    {
        #region Abstract

        public void AlertPropertyChanging(string propertyName)
        {
            OnPropertyChanging(propertyName);
        }

        public void AlertPropertyChanged(string propertyName)
        {
            OnPropertyChanged(propertyName);
        }

        protected void OnPropertyChanged(string propertyName)
        {
            var handler = PropertyChanged;
            if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }

        protected void OnPropertyChanging(string propertyName)
        {
            var handler = PropertyChanging;
            if (handler != null) handler(this, new PropertyChangingEventArgs(propertyName));
        }

        protected bool Set<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
        {
            if (EqualityComparer<T>.Default.Equals(field, value)) return false;
            OnPropertyChanging(propertyName);
            field = value;
            OnPropertyChanged(propertyName);
            return true;
        }

        protected void Set(Action action, string propertyName = null)
        {
            OnPropertyChanging(propertyName);
            if (action != null) action();
            OnPropertyChanged(propertyName);
        }

        #endregion

        #region Implementation of INotifyPropertyChanged

        public event PropertyChangedEventHandler PropertyChanged;

        #endregion Implementation of INotifyPropertyChanged

        #region Implementation of INotifyPropertyChanging

        public event PropertyChangingEventHandler PropertyChanging;

        #endregion Implementation of INotifyPropertyChanging
    }

    public class MySubDataTest : ViewModelBase
    {
        public MySubDataTest()
        {
        }

        public string Test
        {
            get { return "SubTest"; }
            set { }
        }


        public bool IsExpanded
        {
            get { return m_IsExpanded; }
            set { Set(ref m_IsExpanded, value); }
        }

        private bool m_IsExpanded = false;


    }


    public class MyDataTest : ViewModelBase
    {
        public MyDataTest()
        {
            int test = 1000;
            for (int i = 0; i < test; i++)
            {
                Collection.Add(new MySubDataTest());
            }
        }

        public string Test
        {
            get { return "Test"; }
            set {  }
        }


        public bool IsExpanded
        {
            get { return m_IsExpanded; }
            set { Set(ref m_IsExpanded, value); }
        }

        private bool m_IsExpanded = false;

        public ObservableCollection<MySubDataTest> Collection
        {
            get { return m_Collection; }
        }

        ObservableCollection<MySubDataTest> m_Collection = new ObservableCollection<MySubDataTest>();
    }

    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        public ObservableCollection<MyDataTest> Collection
        {
            get { return m_Collection; }
        }

        ObservableCollection<MyDataTest> m_Collection = new ObservableCollection<MyDataTest>();

        private void ButtonBase_OnClick(object _sender, RoutedEventArgs _e)
        {
            int count = 1;
            for (var i = 0; i < count; i++)
            {
                    Collection.Add(new MyDataTest());
            }
            DataContext = this;
        }

        private void ButtonBase_OnClick2(object _sender, RoutedEventArgs _e)
        {
            foreach (MyDataTest test in Collection)
            {
                foreach (MySubDataTest sub in test.Collection)
                {
                    sub.IsExpanded = true;
                }
                test.IsExpanded = true;
            }
        }
    }
}

Thank you in advance.


Solution

  • There is no real out of the box way to do this. The way I did it in the end was to fully customise the Templates of a TreeView as it supports heirarchical virtualization.

    You can use this, along with DataTemplates to produce the same outcome as you can with recursive ItemsTemplates and its much more efficent.