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

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

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

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

这里写图片描述

这里写图片描述
参考链接

原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 绿萝花叶子黄了怎么办 月花叶子黄了怎么办啊 养的花叶子发黄怎么办 桅子花叶子发黄怎么办 膨珊瑚变皱了怎么办 狗狗的脚断了怎么办 拔倒刺手指肿了怎么办 多肉发芽徒长了怎么办 黄丽又大又肥怎么办 小叶紫檀盘坏了怎么办 多肉植物烂根怎么办 檀木手串开裂了怎么办 手串越来越黑了怎么办 绿萝叶子尖发黄怎么办 绿萝叶子发黄有黑斑怎么办 绿萝叶子尖干枯怎么办 养的花叶子干了怎么办 云株花叶子发黄怎么办 驱蚊草叶子发黄怎么办 多肉叶子有黑点怎么办 多肉植物干瘪了怎么办 多肉叶片有黑点怎么办 多肉叶片有黑斑怎么办 多肉表面有黑点怎么办 杉果游戏买了后怎么办 盆景叶子上的灰怎么办 福建茶盆景掉叶怎么办 被普通蜘蛛咬了怎么办 被小蜘蛛咬了怎么办 被一般蜘蛛咬了怎么办 联通4g流量超出怎么办 腿上的肥胖纹怎么办除 金属眼镜腿歪了怎么办 炸东西油往外溢怎么办 油反复使用起沫怎么办 炸东西的油黑了怎么办 板栗放久了干了怎么办 三四个月的宝宝拉肚子怎么办 四个月的孩子拉肚子怎么办 4个月孩子拉肚子怎么办 三阳的房子很热怎么办