最近在做公司内部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()来打开整个树,这个方法是自带的。
不管怎样,总算解决了……