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

来源:互联网 发布:服装收银软件 编辑:程序博客网 时间:2024/06/06 15:54

最近在做公司内部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()来打开整个树,这个方法是自带的。

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

这里写图片描述

这里写图片描述
参考链接