且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

WPF中ItemsControl应用虚拟化时找到子元素的方法

更新时间:2022-09-23 18:47:29

原文:WPF中ItemsControl应用虚拟化时找到子元素的方法

 wpf的虚拟化技术会使UI的控件只初始化看的到的子元素, 而不是所有子元素都被初始化,这样会提高UI性能。

但是我们经常会遇到一个问题:
应用虚拟化后看不见的子元素因为没有实际产生导致ItemContainerGenerator的查找元素方法(ContainerFromIndex / ContainerFromItem)失效。


解决办法1:
(1)监听ItemsControl的ItemContainerGenerator的StatusChanged事件, 当GeneratorStatus为ContainerGenerated时再进行查找,
(2)遍历ItemsControl的Items,获取当前需要查找元素的Index,
(3)利用反射调用VirtualizingPanel的BringIndexIntoView,或者直接调用BringIntoView,然后强制滚动以产生Item(具体可以参考TreeViewItem的ExpandRecursicve的内部实现)。

需要注意的是:
(1)ItemContainerGenerator的StatuChanged事件会多次被触发, 原因是每次初始化的Item数量就是当前空间所能看到的数量,StatusChanged触发的次数就是总共Items除以每次初始的Item数量。
(2)调用BringIndexInToView不正确会导致InvalidOperationException,具体为“Cannot call StartAt when content generation is in progress.”  或者 ”无法在正在进行内容生成时调用StartAt。”。 可以用Dispatcher.BeginInvoke来解决, 如下面代码。
(3)当然ItemsControl中的虚拟化模式设置为Recycling, 即 VirtualizingStackPanel.VirtualizationMode ="Recycling"时,后端存储的子元素选中项会在ItermContainerGenerator重新产生子项时变为DisconnectedItem。可以把模式设置为Standard解决。


具体代码如下:
  • 1. 查找入口 

        private ItemsControl _currentSelectedItem = null;

        private void BtnFind_Click( object sender , System. Windows.RoutedEventArgs e)
        {
            if (string .IsNullOrEmpty( txtContent.Text ))
            {
                return;
            }

            if (_currentSelectedItem == null)
            {
                _currentSelectedItem = _treeView ;
            }
            else
            {
                if (_currentSelectedItem is TreeViewItem)
                {
                    ( _currentSelectedItem as TreeViewItem). IsExpanded = true ;
                }
            }

            if (_currentSelectedItem .ItemContainerGenerator. Status != GeneratorStatus .ContainersGenerated)
            {
                _currentSelectedItem.ItemContainerGenerator .StatusChanged -= new EventHandler(ItemContainerGenerator_StatusChanged );
                _currentSelectedItem.ItemContainerGenerator .StatusChanged += new EventHandler(ItemContainerGenerator_StatusChanged );
            }
            else
            {
                treeViewItem_BringIntoView(txtContent .Text);
            }
        }

  • 2.StatusChanged事件的处理
      void ItemContainerGenerator_StatusChanged (object sender, EventArgs e)
        {
            var generator = sender as ItemContainerGenerator ;
            if (null == generator)
            {
                return;
            }

            //once the children have been generated, expand those children's children then remove the event handler
            if (generator .Status == GeneratorStatus.ContainersGenerated && _currentSelectedItem .ItemContainerGenerator. Status == GeneratorStatus .ContainersGenerated)
            {
                treeViewItem_BringIntoView(txtContent .Text);
            }
        }

  • 3.具体虚拟化时的强制产生子元素及查找处理
 private void treeViewItem_BringIntoView(string findItem)
        {
            System.Diagnostics. Debug.WriteLine("enter treeViewItem_BringIntoview" );

            try
            {
                _currentSelectedItem.ApplyTemplate();
                ItemsPresenter itemsPresenter = (ItemsPresenter)_currentSelectedItem.Template.FindName("ItemsHost", (FrameworkElement)_currentSelectedItem);
                if (itemsPresenter != null )
                    itemsPresenter.ApplyTemplate();
                else
                    _currentSelectedItem.UpdateLayout();
                VirtualizingPanel virtualizingPanel = _currentSelectedItem.GetItemsHost() as VirtualizingPanel;
                virtualizingPanel.CallEnsureGenerator();

                int selectedIndex = -1;
                int count1 = _currentSelectedItem.Items.Count;
                for (int i = 0; i < count1; i++)
                {
                    ItemsItem1 tviItem = _currentSelectedItem.Items.GetItemAt(i) as ItemsItem1;

                    if (null != tviItem && tviItem.Label.Equals(findItem))
                    {
                        selectedIndex = i;

                        break;
                    }
                }

                if (selectedIndex < 0)
                {
                    return;
                }

                Action action = () =>
                {
                    TreeViewItem itemSelected = null ;

                    //Force to generate every treeView item by using scroll item
                    if (virtualizingPanel != null )
                    {
                        try
                        {
                            virtualizingPanel.CallBringIndexIntoView(selectedIndex);
                        }
                        catch (System.Exception ex)
                        {
                            System.Diagnostics. Debug.WriteLine("CallBringIndexIntoView exception : " + ex.Message);
                        }

                        itemSelected = (TreeViewItem)_currentSelectedItem.ItemContainerGenerator.ContainerFromIndex(selectedIndex);
                    }
                    else
                    {
                        itemSelected = (TreeViewItem)_currentSelectedItem.ItemContainerGenerator.ContainerFromIndex(selectedIndex);
                        itemSelected.BringIntoView();
                    }

                    if (null != itemSelected)
                    {
                        _currentSelectedItem = itemSelected;
                        (_currentSelectedItem as TreeViewItem ).IsSelected = true;
                        _currentSelectedItem.BringIntoView();
                    }
                };

                Dispatcher.BeginInvoke( DispatcherPriority.Background, action);
            }
            catch (System.Exception ex)
            {
                //
            }
        }

  • 4.xaml代码

<Window
    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"
    mc:Ignorable ="d"
    x:Class ="WpfApplication1.MainWindow"
    x:Name ="Window"
    Title="MainWindow"
    Width="640"
    Height="480" >
    <Window.Resources>
        <HierarchicalDataTemplate
            x:Key ="ItemsItem1Template"
            ItemsSource="{Binding Items}" >
            <StackPanel>
                <TextBlock
                    Text="{Binding Label}" />
            </StackPanel>
        </HierarchicalDataTemplate>
    </Window.Resources>

    <Grid
        x:Name ="LayoutRoot" >
        <TreeView
            x:Name ="_treeView"
            HorizontalAlignment="Left"
            Width="287"
            d:DataContext ="{Binding}"
            ItemsSource="{Binding Items, Source ={StaticResource SampleDataSource }}"
            ItemTemplate="{DynamicResource ItemsItem1Template}"
            VirtualizingStackPanel.IsVirtualizing ="True"
             VirtualizingStackPanel.VirtualizationMode ="Standard"
              />
        <Button
            x:Name ="btnFind"
            Content="Find"
            HorizontalAlignment="Right"
            Margin="0,8,8,0"
            VerticalAlignment="Top"
            Width="75"
            Click="BtnFind_Click" />
        <TextBox
            x:Name ="txtContent"
            Margin="291,8,87,0"
            TextWrapping="Wrap"
            VerticalAlignment="Top"
            Height="21.837" />
    </Grid>
</Window>

  • 5.反射系统控件私有方法的类

public static class WPFUIElementExtension
    {
        #region Functions to get internal members using reflection

        // Some functionality we need is hidden in internal members, so we use reflection to get them

        #region ItemsControl.ItemsHost

        static readonly PropertyInfo ItemsHostPropertyInfo = typeof (ItemsControl). GetProperty("ItemsHost" , BindingFlags.Instance | BindingFlags. NonPublic);

        public static Panel GetItemsHost(this ItemsControl itemsControl)
        {
            Debug.Assert (itemsControl != null);
            return ItemsHostPropertyInfo .GetValue( itemsControl, null ) as Panel;
        }

        #endregion ItemsControl.ItemsHost

        #region Panel.EnsureGenerator

        private static readonly MethodInfo EnsureGeneratorMethodInfo = typeof(Panel ).GetMethod( "EnsureGenerator", BindingFlags .Instance | BindingFlags.NonPublic );

        public static void CallEnsureGenerator(this Panel panel)
        {
            Debug.Assert (panel != null);
            EnsureGeneratorMethodInfo.Invoke (panel, null);
        }

        #endregion Panel.EnsureGenerator

        #region VirtualizingPanel. BringIndexIntoView

        private static readonly MethodInfo BringIndexIntoViewMethodInfo = typeof(VirtualizingPanel ).GetMethod( "BringIndexIntoView", BindingFlags .Instance | BindingFlags.NonPublic );

        public static void CallBringIndexIntoView(this VirtualizingPanel virtualizingPanel, int index)
        {
            Debug.Assert (virtualizingPanel != null);
            BringIndexIntoViewMethodInfo.Invoke (virtualizingPanel, new object [] { index });
        }

        #endregion VirtualizingPanel. BringIndexIntoView

        #endregion Functions to get internal members using reflection
    }


解决方法2:
(1)参考方法1的第一步解决方法
(2)遍历ItemsControl的Items, 根据ContainerFromIndex去找到当前可见的元素的index。
(3)利用BringInoView去滚动现有的Item以便UI产生后续的子元素, 然后循环直到找见要查找的子元素。(遍历分为2部分,向前遍历和向后遍历)

注意事项:
(1)参考方法1的第一注意事项
(2)因为比方法1多了一次循环遍历,当items很多时有点卡顿,不过还在可以忍受的范围。

具体代码:
1.参考方法1的代码,具体只有强制生成子元素的方法有区别, 即与方法1中的步骤3有区别。
2.如下:

  private void treeViewItem_BringIntoView2(string findItem)
        {
            System.Diagnostics. Debug .WriteLine("enter treeViewItem_BringIntoview" );

            try
            {
                _currentSelectedItem.ApplyTemplate();
                ItemsPresenter itemsPresenter = (ItemsPresenter )_currentSelectedItem.Template.FindName( "ItemsHost", (FrameworkElement )_currentSelectedItem);
                if (itemsPresenter != null )
                    itemsPresenter.ApplyTemplate();
                else
                    _currentSelectedItem.UpdateLayout();
                VirtualizingPanel virtualizingPanel = _currentSelectedItem.GetItemsHost() as VirtualizingPanel ;
                virtualizingPanel.CallEnsureGenerator();

                TreeViewItem itemTemp = null ;
                ItemsItem1 objTemp = null ;
                int visiableIndex = -1;
                int findIndex = -1;
                int count1 = _currentSelectedItem.Items.Count;
                for (int i = 0; i < count1; i++)
                {
                    itemTemp = ( TreeViewItem )_currentSelectedItem.ItemContainerGenerator.ContainerFromIndex(i);
                    if (null != itemTemp)
                    {
                        visiableIndex = i;
                    }

                    objTemp = _currentSelectedItem.Items.GetItemAt(i) as ItemsItem1 ;
                    if (null != objTemp && objTemp.Label.Equals(findItem))
                    {
                        findIndex = i;
                    }
                }

                if (findIndex == -1 || visiableIndex == -1)
                {
                    return ;
                }

                if (findIndex < visiableIndex)
                {
                    for (int j = visiableIndex; j >= findIndex; j--)
                    {
                        itemTemp = (TreeViewItem )_currentSelectedItem.ItemContainerGenerator.ContainerFromIndex(j);
                        if (null != itemTemp)
                        {
                            itemTemp.BringIntoView();
                        }
                    }
                }
                else if (findIndex > visiableIndex)
                {
                    for (int j = visiableIndex; j <= findIndex; j++)
                    {
                        itemTemp = (TreeViewItem )_currentSelectedItem.ItemContainerGenerator.ContainerFromIndex(j);
                        if (null != itemTemp)
                        {
                            itemTemp.BringIntoView();
                        }
                    }
                }
                else
                {
                    itemTemp = (TreeViewItem )_currentSelectedItem.ItemContainerGenerator.ContainerFromIndex(visiableIndex);
                    if (null != itemTemp)
                    {
                        itemTemp.BringIntoView();
                    }
                }

                if (null != itemTemp)
                {
                    _currentSelectedItem = itemTemp;
                    (_currentSelectedItem as TreeViewItem ).IsSelected = true;
                    _currentSelectedItem.BringIntoView();
                }

            }
            catch (System.Exception ex)
            {
                //
            }
        }