WPF TreeView大数据量多层级搜索定位

最近在做公司内部IM,使用的是网易云信SDK,有需要的同学可以去了解一下。
今天主要说一说公司组织架构这一块,需求是在搜索框输入员工姓名或者首字母,搜索框实时自动匹配到存在的员工,选中某一员工后在组织结构层级树中定位到该员工,就类似于PC版QQ的搜索框。
综上,我们涉及到的控件主要有两个:1.搜索框 2.TreeView
了解WPF的同学肯定立马会想到这个搜索框应该用AutoCompleteBox来做了,没错,我们先通过NuGet引入WPFToolkit,然后在对应的xaml页面引入命名空间:
xmlns:tookit=”clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Input.Toolkit”

下来就可以使用了,

 <tookit:AutoCompleteBox  x:Name="searchControl"
                                MinimumPopulateDelay="100"
                                ValueMemberPath="Search"
                                FilterMode="Custom"
                                DropDownClosing="SearchControl_DropDownClosing"
                                Style="{DynamicResource AutoCompleteBoxStyle1}">
                <tookit:AutoCompleteBox.ItemTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding Search}"/>
                    </DataTemplate>
                </tookit:AutoCompleteBox.ItemTemplate>
 </tookit:AutoCompleteBox>

我这边赋值了3个属性1个事件:
MinimumPopulateDelay=”100”//用户停止输入后多久触发自动匹配,单位毫秒
ValueMemberPath=”Search”//后台对应的关键词属性,即根据实体中的“Search”字段来匹配
FilterMode=”Custom”//自定义过滤模式,需后台代码支持
DropDownClosing=”SearchControl_DropDownClosing”//在这个事件里处理关键词匹配

遗憾的是tookit:AutoCompleteBox没有水印功能,我们只好自己实现一下,眼尖的同学肯定已经看到AutoCompleteBoxStyle1这个样式了:

  <Style x:Key="AutoCompleteBoxStyle1" TargetType="{x:Type tookit:AutoCompleteBox}">
            <Setter Property="IsTabStop" Value="True"/>
            <Setter Property="BorderThickness" Value="0"/>
            <Setter Property="BorderBrush">
                <Setter.Value>
                    <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                        <GradientStop Color="#FFA3AEB9" Offset="0"/>
                        <GradientStop Color="#FF8399A9" Offset="0.375"/>
                        <GradientStop Color="#FF718597" Offset="0.375"/>
                        <GradientStop Color="#FF617584" Offset="1"/>
                    </LinearGradientBrush>
                </Setter.Value>
            </Setter>
            <Setter Property="Background" Value="Transparent"/>
            <Setter Property="Foreground" Value="Black"/>
            <Setter Property="MinWidth" Value="45"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type tookit:AutoCompleteBox}">
                        <Border CornerRadius="12 12 0 0"  Background="#65D1DF">
                            <Grid Opacity="{TemplateBinding Opacity}" >
                                <Grid>
                                    <StackPanel Orientation="Horizontal" Visibility="{Binding ElementName=Text,Path=Text.Length,Converter={StaticResource AutoCompeleteBoxWaterMarkConverter}}" >
                                        <Image Source="../Resources/Images/icon_fangdajing.png" HorizontalAlignment="Left" Width="12" Margin="8 0 0 0"/>
                                        <TextBlock Text="搜索用户" Foreground="#ffffff" VerticalAlignment="Center" Padding="4 0 0 0" ></TextBlock>
                                    </StackPanel>
                                    <TextBox x:Name="Text"  BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Foreground="{TemplateBinding Foreground}" IsTabStop="True" Margin="0" Padding="{TemplateBinding Padding}"/>
                                </Grid>
                                <Popup x:Name="Popup">
                                    <Grid Background="{TemplateBinding Background}" >
                                        <Border x:Name="PopupBorder" BorderThickness="0" Background="#11000000" HorizontalAlignment="Stretch" Opacity="1">
                                            <Border.RenderTransform>
                                                <TranslateTransform X="1" Y="1"/>
                                            </Border.RenderTransform>
                                            <Border BorderBrush="#65D1DF" 
                                                BorderThickness="1"
                                                CornerRadius="0"
                                                HorizontalAlignment="Stretch" 
                                                Opacity="1"
                                                Padding="0"
                                                Background="#ffffff">
                                                <Border.RenderTransform>
                                                    <TransformGroup>
                                                        <TranslateTransform X="-1" Y="-1"/>
                                                    </TransformGroup>
                                                </Border.RenderTransform>
                                                <ListBox x:Name="Selector"  BorderThickness="0" Background="{TemplateBinding Background}" Foreground="{TemplateBinding Foreground}" ScrollViewer.HorizontalScrollBarVisibility="Auto" ItemTemplate="{TemplateBinding ItemTemplate}" ItemContainerStyle="{TemplateBinding ItemContainerStyle}" ScrollViewer.VerticalScrollBarVisibility="Auto"/>
                                            </Border>
                                        </Border>
                                    </Grid>
                                </Popup>
                            </Grid>
                        </Border>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

我是加了一个StackPanel,其中有我们的“水印”:一个image+一个TextBlock,通过AutoCompeleteBoxWaterMarkConverter来控制“水印”显示与否:

public class AutoCompeleteBoxWaterMark:IValueConverter
{
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if (value==null)
            {
                return Visibility.Visible;
            }
            int length = (int)value;
            if (length > 0)
            {
                return Visibility.Collapsed;
            }
            return Visibility.Visible;
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }
}

注意:要引用自定义的converter,同样需要引入对应的命名空间。

下面开始组装数据,由于后台给到的数据是包含层级的,所以我们可以直接将数据绑定到TreeView的数据源,但是对于tookit:AutoCompleteBox的数据我们就需要处理一下了,可以用递归将原来有层级的数据放到一个list里,将该list绑定到tookit:AutoCompleteBox数据源上,tookit:AutoCompleteBox就可以通过这个源数据来匹配用户输入了,这里我就省去组装代码直接上结果了:
this.userTreeControl.ItemsSource = groups; //treeview数据源
this.searchControl.ItemsSource = users; //tookit:AutoCompleteBox数据源

下面开始定义tookit:AutoCompleteBox的Filter,先在构造函数里注册一下:

this.searchControl.ItemFilter += SearchControl_ItemFilter;

然后实现SearchControl_ItemFilter,该方法中两个helper由于代码太长这里就不贴了,网上都可以找到,同时我也上传到我的资源中心了:

 private bool SearchControl_ItemFilter(string search, object item)
 {
            string text = CommonHelper.GetPropertyValue(item, "Search").ToString().ToLower();
            string tmp = text.Split(':').FirstOrDefault();
            text += ChineseCharHelper.GetFirstLetter(tmp).ToLower();
            return text.Contains(search.ToLower());
 }

以上,tookit:AutoCompleteBox算是完成了。

下面开始说TreeView,在tookit:AutoCompleteBox的DropDownClosing()事件中通过this.userTreeControl.SelectItem(result) 这行代码来触发在TreeView中定位,SelectItem()方法代码如下,这段代码网上比较多,百度TreeViewHelper即可:

 /// <summary>
        /// Searches a TreeView for the provided object and selects it if found
        /// </summary>
        /// <param name="treeView">The TreeView containing the item</param>
        /// <param name="item">The item to search and select</param>
        public static void SelectItem(this TreeView treeView, object item)
        {
            ExpandAndSelectItem(treeView, item);
        }
        /// <summary>
        /// Finds the provided object in an ItemsControl's children and selects it
        /// </summary>
        /// <param name="parentContainer">The parent container whose children will be searched for the selected item</param>
        /// <param name="itemToSelect">The item to select</param>
        /// <returns>True if the item is found and selected, false otherwise</returns>
        private static bool ExpandAndSelectItem(ItemsControl parentContainer, object itemToSelect)
        {
            //check all items at the current level
            foreach (Object item in parentContainer.Items)
            {
                TreeViewItem currentContainer = parentContainer.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;
                //if the data item matches the item we want to select, set the corresponding
                //TreeViewItem IsSelected to true
                if (item == itemToSelect && currentContainer != null)
                {
                    currentContainer.IsSelected = true;
                    currentContainer.BringIntoView();
                    currentContainer.Focus();
                    //the item was found
                    return true;
                }
            }
            //if we get to this point, the selected item was not found at the current level, so we must check the children
            foreach (Object item in parentContainer.Items)
            {
                TreeViewItem currentContainer = parentContainer.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;
                //if children exist
                if (currentContainer != null && currentContainer.Items.Count > 0)
                {
                    //keep track of if the TreeViewItem was expanded or not
                    bool wasExpanded = currentContainer.IsExpanded;
                    //expand the current TreeViewItem so we can check its child TreeViewItems
                    currentContainer.IsExpanded = true;
                    //if the TreeViewItem child containers have not been generated, we must listen to
                    //the StatusChanged event until they are
                    if (currentContainer.ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated)
                    {
                        //store the event handler in a variable so we can remove it (in the handler itself)
                        EventHandler eh = null;
                        eh = new EventHandler(delegate
                        {
                            if (currentContainer.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
                            {
                                if (ExpandAndSelectItem(currentContainer, itemToSelect) == false)
                                {
                                    //The assumption is that code executing in this EventHandler is the result of the parent not
                                    //being expanded since the containers were not generated.
                                    //since the itemToSelect was not found in the children, collapse the parent since it was previously collapsed
                                    currentContainer.IsExpanded = false;
                                }
                                //remove the StatusChanged event handler since we just handled it (we only needed it once)
                                currentContainer.ItemContainerGenerator.StatusChanged -= eh;
                            }
                        });
                        currentContainer.ItemContainerGenerator.StatusChanged += eh;
                    }
                    else //otherwise the containers have been generated, so look for item to select in the children
                    {
                        if (ExpandAndSelectItem(currentContainer, itemToSelect) == false)
                        {
                            //restore the current TreeViewItem's expanded state
                            currentContainer.IsExpanded = wasExpanded;
                        }
                        else //otherwise the node was found and selected, so return true
                        {
                            return true;
                        }
                    }
                }
            }
            //no item was found
            return false;
        }

到这里,功能部分已经完成了,然后当我选中某个员工,发现得有15秒TreeView才能自动定位到该员工,但是当我第2次搜索的时候又会变得非常快了,这是什么原因呢,看ExpandAndSelectItem()方法我们就会知道,因为我们只是给TreeView绑定了数据源,但是这个TreeView还未被打开过,等于说它的子树还没生成,于是ExpandAndSelectItem()帮我们把每一层级都打开,这明显是个耗时的过程,当我们第2次再搜索的时候这棵树已经被相当于被打开过了,所以会变的很快。
另外,这里慢跟数据量和层级多少也是有关系的,如果像QQ那样只有一层,又或者只有百十来条数据,那自然不会很慢。我这边大概4000条数据,4~5层层级。
15秒显然是不能忍受的,那么这个问题怎么解决呢?这里提供一个讨巧的方法,既然没被打开过,那我初始化的时候打开一遍不就行了!如果你介意打开后影响美观,那再关上就是了。

//打开
public static void ExpandAllSubtree(this TreeView treeView)
{
       foreach (var t in treeView.Items)
       {
           DependencyObject o = treeView.ItemContainerGenerator.ContainerFromItem(t);
           ((TreeViewItem)o).ExpandSubtree();
       }
}

//关闭        
public static void CollapseAll(this TreeView treeView)
{
       CollapseTreeViewItems(treeView);
}

private static void CollapseTreeViewItems(ItemsControl parentContainer)
{
       foreach (var item in parentContainer.Items)
       {
           DependencyObject o = parentContainer.ItemContainerGenerator.ContainerFromItem(item);
           if (o != null)
           {
                ((TreeViewItem)o).IsExpanded = false;
                if (((TreeViewItem)o).HasItems)
                {
                    CollapseTreeViewItems(((TreeViewItem)o));
                 }
            }
       }
}

那么对TreeView初始化的时候就变成这样:
this.userTreeControl.ItemsSource = groups;
this.userTreeControl.ExpandAllSubtree();
this.userTreeControl.CollapseAll();

其实,TreeViewHelper里也有个打开整棵树的方法ExpandAll(),但是通过这个方法同样需要15秒,看一下代码就可以知道,这个方法基本和SelectItem()是一样的,都要针对尚未打开的子树进行StatusChanged事件的注册与注销,耗时即在此。所以我选择用ExpandSubtree()来打开整个树,这个方法是自带的。

不管怎样,总算解决了……
这里写图片描述

这里写图片描述

这里写图片描述
参考链接

猜你喜欢

转载自blog.csdn.net/leebin_20/article/details/72677485