在 ItemsHost 面板上调用 Measure 后,无法更改 ItemsControl 上附加的 VirtualizationMode 属性

Cannot change the VirtualizationMode attached property on an ItemsControl after Measure is called on the ItemsHost panel

当我尝试将 ListView 上的 VirtualizationMode 设置为 Recycling 时,我从标题中得到错误:

Cannot change the VirtualizationMode attached property on an ItemsControl after Measure is called on the ItemsHost panel.

我正在尝试以编程方式设置附加的 属性,但是当我尝试在 XAML 中定义 VirtualizationMode 时,设计器从标题中抛出相同的错误。有人遇到过类似的问题吗?

我在XAML中的观点是:

<Window x:Class="FinalVirtualizationApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:FinalVirtualizationApp"
        mc:Ignorable="d"
        Title="MainWindow" Height="900" Width="800"
        xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
         TextElement.Foreground="{DynamicResource MaterialDesignBody}"
         TextElement.FontWeight="Regular"
         TextElement.FontSize="13"
         TextOptions.TextFormattingMode="Ideal"
         TextOptions.TextRenderingMode="Auto"
         Background="{DynamicResource MaterialDesignPaper}"
         FontFamily="{DynamicResource MaterialDesignFont}">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
        </Grid.ColumnDefinitions>

        <GroupBox Grid.Row="0" Header="UI virtualization options">
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition/>
                    <RowDefinition/>
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition/>
                    <ColumnDefinition/>
                </Grid.ColumnDefinitions>
                <CheckBox Content="UI virtualization" VerticalAlignment="Center" IsChecked="{Binding IsUIVirtualization}"/>
                <Grid Grid.Column="1">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="Auto"/>
                        <ColumnDefinition/>
                    </Grid.ColumnDefinitions>
                    <Label Content="Container Recycling:" VerticalAlignment="Center"/>
                    <ComboBox Grid.Column="1" VerticalAlignment="Center" SelectedValue="{Binding ContainerRecyclingType}" SelectedValuePath="Content">
                        <ComboBoxItem>Recycling</ComboBoxItem>
                        <ComboBoxItem>Standard</ComboBoxItem>
                    </ComboBox>
                </Grid>
                <CheckBox Content="Deferred scrolling" Grid.Row="1" VerticalAlignment="Center" IsChecked="{Binding IsDeferredScrolling}">

                </CheckBox>
                <Grid Grid.Row="1" Grid.Column="1">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="Auto"/>
                        <ColumnDefinition Width="74*"/>
                        <ColumnDefinition Width="253*"/>
                    </Grid.ColumnDefinitions>
                    <Label Content="Scroll unit" VerticalAlignment="Center" Margin="0,0,0,1"/>
                    <ComboBox Grid.Column="1" VerticalAlignment="Center" SelectedValue="{Binding ScrollUnitType}" SelectedValuePath="Content" Grid.ColumnSpan="2" Margin="0,2,0,3">
                        <ComboBoxItem>Item</ComboBoxItem>
                        <ComboBoxItem>Pixel</ComboBoxItem>
                    </ComboBox>
                </Grid>
            </Grid>
        </GroupBox>
        <GroupBox Grid.Row="1"  Header="Data virtualization">
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition/>
                    <RowDefinition/>
                </Grid.RowDefinitions>
                <GroupBox Header="ItemsProvider">
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition/>
                            <ColumnDefinition/>
                        </Grid.ColumnDefinitions>
                        <StackPanel Orientation="Horizontal">
                            <Label Content="Number of Items: "/>
                            <TextBox Width="60" Text="{Binding NumberOfItems}"/>
                        </StackPanel>
                        <StackPanel Grid.Column="1" Orientation="Horizontal">
                            <Label Content="Fetch delay(ms): "/>
                            <TextBox Width="60" Text="{Binding FetchDelay}"/>
                        </StackPanel>
                    </Grid>
                </GroupBox>
                <GroupBox Grid.Row="1" Header="Collection">
                    <StackPanel>
                        <StackPanel Orientation="Horizontal" Margin="0,2,0,0">
                            <TextBlock Text="Type:" Margin="5" TextAlignment="Right" VerticalAlignment="Center"/>
                            <RadioButton x:Name="rbNormal" GroupName="rbGroup" Margin="5" Content="List(T)" VerticalAlignment="Center" Command="{Binding CollectionTypeChangeCommand}" CommandParameter="List"/>
                            <RadioButton x:Name="rbVirtualizing" GroupName="rbGroup" Margin="5" Content="VirtualizingList(T)" VerticalAlignment="Center" Command="{Binding CollectionTypeChangeCommand}" CommandParameter="VirtualizingList"/>
                            <RadioButton x:Name="rbAsync" GroupName="rbGroup" Margin="5" Content="AsyncVirtualizingList(T)"  VerticalAlignment="Center" Command="{Binding CollectionTypeChangeCommand}" CommandParameter="AsyncVirtualizingList"/>
                        </StackPanel>
                        <StackPanel Orientation="Horizontal" Margin="0,2,0,0">
                            <TextBlock Text="Page size:" Margin="5" TextAlignment="Right" VerticalAlignment="Center"/>
                            <TextBox x:Name="tbPageSize" Margin="5" Text="{Binding PageSize}" Width="60" VerticalAlignment="Center"/>
                            <TextBlock Text="Page timeout (s):" Margin="5" TextAlignment="Right" VerticalAlignment="Center"/>
                            <TextBox x:Name="tbPageTimeout" Margin="5" Text="{Binding PageTimeout}" Width="60" VerticalAlignment="Center"/>
                        </StackPanel>
                    </StackPanel>
                </GroupBox>
            </Grid>
        </GroupBox>
        <StackPanel Orientation="Horizontal" Grid.Row="2">
            <TextBlock Text="Memory Usage:" Margin="5" VerticalAlignment="Center"/>
            <TextBlock x:Name="tbMemory" Margin="5" Width="80" Text="{Binding MemoryUsage}" VerticalAlignment="Center"/>

            <Button Content="Refresh" Margin="5" Width="100" VerticalAlignment="Center" Command="{Binding RefreshCommand}"/>
        </StackPanel>
        <ListView Grid.Row="3" Name="lvItems">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <Expander Header="{Binding DeviceName}">
                        <StackPanel>

                        </StackPanel>
                    </Expander>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </Grid>
</Window>

我尝试在此处设置 VirtualizationMode

public void SetUIVirtualizationOptions()
{
    listView.SetValue(VirtualizingStackPanel.IsVirtualizingProperty, IsUIVirtualization);
    listView.SetValue(ScrollViewer.IsDeferredScrollingEnabledProperty, IsDeferredScrolling);

    if(ContainerRecyclingType == "Recycling")
        listView.SetValue(VirtualizingStackPanel.VirtualizationModeProperty, VirtualizationMode.Recycling);
    else
        listView.SetValue(VirtualizingStackPanel.VirtualizationModeProperty, VirtualizationMode.Standard);


    if (ScrollUnitType == "Item")
        listView.SetValue(VirtualizingPanel.ScrollUnitProperty, ScrollUnit.Item);
    else
        listView.SetValue(VirtualizingPanel.ScrollUnitProperty, ScrollUnit.Pixel);
}

编辑:我将列表视图传递给视图模型的 Window 代码是:

    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = new MainWindowViewModel(lvItems);
        }
    }

以及代码的视图模型部分(减去 getter 和 setter 以减少 space 采取):

public class MainWindowViewModel : BindableBase
    {
        #region private fields
        private ListView listView;
        private bool isUIVirtualization;
        private bool isDeferredScrolling;
        private string containerRecyclingType;
        private string scrollUnitType;
        private int numberOfItems;
        private int fetchDelay;
        private string collectionType;
        private int pageSize;
        private int pageTimeout;
        private string memoryUsage;
        private DemoItemProvider itemsProvider;
        #endregion

        #region commands
        public RelayCommand<string> CollectionTypeChangeCommand { get; set; }
        public RelayCommand RefreshCommand { get; set; }
        public RelayCommand<string> ContainerRecyclingTypeChangeCommand { get; set; }
        public RelayCommand<string> ScrollUnitTypeChangeCommand { get; set; }
        #endregion

        public MainWindowViewModel(ListView lvItems)
        {
            this.listView = lvItems;
            PageSize = 100;
            PageTimeout = 30;
            NumberOfItems = 1000000;
            FetchDelay = 1000;

            CollectionTypeChangeCommand = new RelayCommand<string>(CollectionTypeChangeFunc);
            RefreshCommand = new RelayCommand(RefreshFunc);
            ContainerRecyclingTypeChangeCommand = new RelayCommand<string>(ContainerRecyclingTypeChangeFunc);
            ScrollUnitTypeChangeCommand = new RelayCommand<string>(ScrollUnitTypeChangeFunc);

            // use a timer to periodically update the memory usage
            DispatcherTimer timer = new DispatcherTimer();
            timer.Interval = new TimeSpan(0, 0, 1);
            timer.Tick += timer_Tick;
            timer.Start();
        }

        private void timer_Tick(object sender, EventArgs e)
        {
            MemoryUsage = string.Format("{0:0.00} MB", GC.GetTotalMemory(true) / 1024.0 / 1024.0);
        }

        #region command methods
        public void CollectionTypeChangeFunc(string type)
        {
            CollectionType = type;
        }

        public void RefreshFunc()
        {
            SetUIVirtualizationOptions();
            itemsProvider = new DemoItemProvider(NumberOfItems, FetchDelay);


            if (collectionType == "List")
            {
                listView.ItemsSource = new List<DataItem>(itemsProvider.FetchRange(0, itemsProvider.FetchCount()));
            }
            else if (collectionType == "VirtualizingList")
            {
                listView.ItemsSource = new VirtualizingCollection<DataItem>(itemsProvider, pageSize);
            }
            else if (collectionType == "AsyncVirtualizingList")
            {
                listView.ItemsSource = new AsyncVirtualizingCollection<DataItem>(itemsProvider, pageSize, pageTimeout * 1000);
            }
        }

        public void ContainerRecyclingTypeChangeFunc(string type)
        {
            ContainerRecyclingType = type;
        }

        public void ScrollUnitTypeChangeFunc(string type)
        {
            ScrollUnitType = type;
        }

        public void SetUIVirtualizationOptions()
        {
            listView.SetValue(VirtualizingStackPanel.IsVirtualizingProperty, IsUIVirtualization);
            listView.SetValue(ScrollViewer.IsDeferredScrollingEnabledProperty, IsDeferredScrolling);

            if(ContainerRecyclingType == "Recycling")
                listView.SetValue(VirtualizingStackPanel.VirtualizationModeProperty, VirtualizationMode.Recycling);
            else
                listView.SetValue(VirtualizingStackPanel.VirtualizationModeProperty, VirtualizationMode.Standard);


            if (ScrollUnitType == "Item")
                listView.SetValue(VirtualizingPanel.ScrollUnitProperty, ScrollUnit.Item);
            else
                listView.SetValue(VirtualizingPanel.ScrollUnitProperty, ScrollUnit.Pixel);
        }
        #endregion
    }

VirtualizingStackPanelcode source表示这个属性只能在面板初始化前设置:

/// <summary>
///     Attached property for use on the ItemsControl that is the host for the items being 
///     presented by this panel. Use this property to modify the virtualization mode.
/// 
///     Note that this property can only be set before the panel has been initialized 
/// </summary>
public static readonly DependencyProperty VirtualizationModeProperty = 
    DependencyProperty.RegisterAttached("VirtualizationMode", typeof(VirtualizationMode), typeof(VirtualizingStackPanel),
        new FrameworkPropertyMetadata(VirtualizationMode.Standard));

你确实可以检查这个行为 later in the file:

//
// Set up info on first measure
//
if (HasMeasured)
{
    VirtualizationMode oldVirtualizationMode = InRecyclingMode ? VirtualizationMode.Recycling : VirtualizationMode.Standard;
    if (oldVirtualizationMode != virtualizationMode)
    {
        throw new InvalidOperationException(SR.Get(SRID.CantSwitchVirtualizationModePostMeasure));
    }
}
else
{
    HasMeasured = true;
}

并且没有办法(根据源代码)将此 HasMeasured 属性 设置回 False,除非您销毁并重新创建 ListView

如消息所说:

Your not allowed to _change the VirtualizationMode attached property on an ItemsControl after Measure is called on the ItemsHost panel.

这意味着,如果 ListView 已经显示正在使用的虚拟化机制并且您不能更改它。

如果您将XAML中的ListViewVisibility设置为Collapsed并且稍后才将其设置为Visible,那么您可以设置VirtualizationMode 在代码后面到希望的值,但只有一次(所以你不能在 ListView 变得可见后更改它)!

private void Button_Click(object sender, RoutedEventArgs e)
{
    listView.SetValue(VirtualizingStackPanel.VirtualizationModeProperty, VirtualizationMode.Recycling);
    listView.Visibility = Visibility.Visible;
}

XAML:

<ListView x:Name="listView" ... Visibility="Collapsed">