在我们展示一些参考信息的时候,有所会用树形列表来展示结构信息,如对于有父子关系的多层级部门机构,以及一些常用如字典大类节点,也都可以利用树形列表的方式进行展示,本篇随笔介绍基于WPF的方式,使用TreeView来洗实现结构信息的展示,以及对它的菜单进行的设置、过滤查询等功能的实现逻辑。
我们前面随笔介绍到的用户信息的展示,左侧就是一个树形的类表,通过展示多层级的部门机构信息,可以快速的查找对应部门的用户信息,如下界面所示。
我们来看看界面中树形列表部分的Xaml代码如下所示。
<TreeView x:Name="deptTree" Margin="0,10,10,0" FontSize="16" ItemsSource="{Binding ViewModel.FilteredTreeItems}" SelectedItemChanged="deptTree_SelectedItemChanged"> <TreeView.ItemContainerStyle> <Style TargetType="{x:Type TreeViewItem}"> <Setter Property="IsExpanded" Value="True" /> </Style> </TreeView.ItemContainerStyle> <TreeView.ItemTemplate> <HierarchicalDataTemplate DataType="{x:Type core:OuNodeInfo}" ItemsSource="{Binding Path=Children}"> <StackPanel Orientation="Horizontal"> <Button hc:IconElement.Geometry="{StaticResource PageModeGeometry}" Background="Transparent" BorderBrush="Transparent" BorderThickness="0" Style="{StaticResource {x:Static ToolBar.ButtonStyleKey}}" /> <Label Padding="-10" Background="Transparent" BorderBrush="Transparent" BorderThickness="0" Content="{Binding Path=Name}" /> </StackPanel> </HierarchicalDataTemplate> </TreeView.ItemTemplate> </TreeView>
其中的ItemsSource是指定TreeView的数据源的,它是一个ItemsControl,因此它有数据源ItemsSource树形,如其他ListBox也是这样的控件基类。
public class TreeView : ItemsControl
而 SelectedItemChanged 是我们在选择不同节点的时候触发的事件,用于我们对数据进行重新查询的处理,实现的代码如下所示。
/// <summary> /// 树列表选中触发事件 /// </summary> private async void deptTree_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e) { var item = e.NewValue as OuNodeInfo; if (item != null) { this.ViewModel.SelectNode = item; await this.ViewModel.GetData(); } }
其中用户列表界面的ViewModel类里面 ,我们定义了一些属性,如下代码所示。
/// <summary> /// 用户列表-视图模型对象 /// </summary> public partial class UserListViewModel : BaseListViewModel<UserInfo, int, UserPagedDto> { /// <summary> /// 查询过滤内容 /// </summary> [ObservableProperty] private string _searchKey = ""; /// <summary> /// 树形数据列表 /// </summary> [ObservableProperty] private List<OuNodeInfo>? treeItems; /// <summary> /// 树形数据列表(过滤列表) /// </summary> [ObservableProperty] private List<OuNodeInfo>? filteredTreeItems; /// <summary> /// 选中的当前节点 /// </summary> [ObservableProperty] private OuNodeInfo selectNode;
刚才我们通过设置选中的节点,然后触发查询GetData就是在UserListViewModel 视图模型类里面,重新构建条件进行处理的,最后还是调用基类BaseListViewModel里面的封装类的实现方法GetData。
/// <summary> /// 设置父类后查询数据 /// </summary> /// <returns></returns> public override Task GetData() { //选中的大类Id if (this.SelectNode != null) { this.PageDto.Dept_ID = this.SelectNode.Id.ToString(); } return base.GetData(); }
而我们的属性列表的数据源,主要就是通过页面Page的构造函数的时候,触发的数据处理,就是GetTreeCommand的调用。
/// <summary> /// UserListPage.xaml 交互逻辑 /// </summary> public partial class UserListPage : INavigableView<UserListViewModel> { /// <summary> /// 视图模型对象 /// </summary> public UserListViewModel ViewModel { get; } /// <summary> /// 构造函数 /// </summary> /// <param name="viewModel">视图模型对象</param> public UserListPage(UserListViewModel viewModel) { ViewModel = viewModel; DataContext = this; InitializeComponent(); //展示树列表 ViewModel.GetTreeCommand.ExecuteAsync(null); }
其中GetTree的命令方法如下所示。
/// <summary> /// 触发处理命令 /// </summary> [RelayCommand] private async Task GetTree() { var treeeNodeList = new List<OuNodeInfo>(); var list = await SecurityHelper.GetMyTopGroup(App.ViewModel!.UserInfo); foreach (var groupInfo in list) { if (groupInfo != null && !groupInfo.IsDeleted) { var node = new OuNodeInfo(groupInfo); var sublist = await BLLFactory<IOuService>.Instance.GetTreeByID(groupInfo.Id); node.Children = sublist; treeeNodeList.Add(node); } } this.TreeItems = treeeNodeList; this.FilteredTreeItems = new List<OuNodeInfo>(this.TreeItems); }
我们通过构建一个用户的顶级部门列表(如管理员可以看全部,公司管理员只能看公司节点),然后给它们填充一个子级的部门列表即可。
另外,在Xaml的界面代码里面,我们可以看到下面的代码
<HierarchicalDataTemplate DataType="{x:Type core:OuNodeInfo}" ItemsSource="{Binding Path=Children}">
这个就是层级树形的内容设置,其中DataType指定对象的类型为OuNodeInfo类,而子节点的的数据源属性名称就是Children属性。
它的内容部分就是我们子节点的呈现界面代码模板了
<TreeView.ItemTemplate> <HierarchicalDataTemplate DataType="{x:Type core:OuNodeInfo}" ItemsSource="{Binding Path=Children}"> <StackPanel Orientation="Horizontal"> <Button hc:IconElement.Geometry="{StaticResource PageModeGeometry}" Background="Transparent" BorderBrush="Transparent" BorderThickness="0" Style="{StaticResource {x:Static ToolBar.ButtonStyleKey}}" /> <Label Padding="-10" Background="Transparent" BorderBrush="Transparent" BorderThickness="0" Content="{Binding Path=Name}" /> </StackPanel> </HierarchicalDataTemplate> </TreeView.ItemTemplate>
同理,对于字典模块的字典大类的展示,由于它们也是多层级的,因此也可以用TreeView进行展示,界面效果如下所示。
在上面的字典模块里面,我们左侧的字典大类,还需要一些右键菜单来实现字典大类的新增、编辑、清空数据项等维护功能的,因此给TreeView设置了右键菜单,如下所示效果。
它的TreeView的Xaml代码如下所示。
<TreeView x:Name="dictTypeTree" Margin="0,10,10,0" ContextMenu="{StaticResource TreeMenu}" FontSize="16" ItemsSource="{Binding ViewModel.FilteredTreeItems}"
其中 ContextMenu="{StaticResource TreeMenu}" 就是设置树列表右键菜单的。
这个部分定义在页面的资源里面,如下代码所示,指定相关菜单的图片、文本,以及对应的Command方法即可。
<Page.Resources> <ContextMenu x:Key="TreeMenu"> <MenuItem Command="{Binding EditDictTypeCommand}" Header="添加字典大类"> <MenuItem.Icon> <Image Source="/Images/Add.png" /> </MenuItem.Icon> </MenuItem> <MenuItem Command="{Binding EditDictTypeCommand}" CommandParameter="{Binding ViewModel.SelectDictType}" Header="编辑字典大类"> <MenuItem.Icon> <Image Source="/Images/edit.png" /> </MenuItem.Icon> </MenuItem> <MenuItem Command="{Binding ViewModel.DeleteTypeCommand}" Header="删除字典大类"> <MenuItem.Icon> <Image Source="/Images/remove.png" /> </MenuItem.Icon> </MenuItem> <MenuItem Command="{Binding ViewModel.GetTreeCommand}" Header="刷新列表"> <MenuItem.Icon> <Image Source="/Images/refresh.png" /> </MenuItem.Icon> </MenuItem> <MenuItem Command="{Binding ViewModel.ClearDataCommand}" Header="清空字典大类数据"> <MenuItem.Icon> <Image Source="/Images/clear.png" /> </MenuItem.Icon> </MenuItem> </ContextMenu> </Page.Resources>
我们来看看后台代码的Comand命令定义,如下是编辑、新增字典大类的处理
/// <summary> /// 新增、编辑字典大类 /// </summary> [RelayCommand] private async Task EditDictType(DictTypeNodeDto info) { //获取新增、编辑页面接口 var page = App.GetService<DictTypeEditPage>(); page!.ViewModel.DictType = this.ViewModel.SelectDictType; if (info != null) { //编辑则接受传入对象 page!.ViewModel.Item = info; page!.ViewModel.IsEdit = true; } else { //新增则初始化 var pid = "-1"; if(this.ViewModel.SelectDictType != null) { pid = this.ViewModel.SelectDictType.Id; } //前后顺序不能变化,否则无法检测属性变化 page!.ViewModel.Item = new DictTypeInfo() { Id=Guid.NewGuid().ToString(), PID = pid, Seq = "001" }; page!.ViewModel.IsEdit = false; } //导航到指定页面 ViewModel.Navigate(typeof(DictTypeEditPage)); }
同样就是获得 选中节点,如果新增作为父节点、如果是编辑,则获得当前节点的信息进行编辑即可。
这个界面里面,需要指定下拉列表的数据源作为父节点,主要就是获取当前所有字典大类即可。这里的下拉列表是ComboBox的控件,控件代码如下所示。
<ComboBox x:Name="txtPID" Grid.Column="0" Margin="5" HorizontalAlignment="Left" hc:InfoElement.TitlePlacement="Left" hc:TitleElement.Title="父项名称" DisplayMemberPath="Text" IsReadOnly="{Binding ViewModel.IsReadOnly, Mode=OneWay}" ItemsSource="{Binding ViewModel.ParentTypeList}" SelectedValue="{Binding ViewModel.Item.PID, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" SelectedValuePath="Value" SelectionChanged="txtPID_SelectionChanged" Style="{StaticResource ComboBoxExtend}" />
在视图模型里面,初始化的时候,把它进行加载即可。
/// <summary> /// 初始化处理字典大类 /// </summary> private async void Init() { //获取字典大类列表 var dictItems = await BLLFactory<IDictTypeService>.Instance.GetAllType(null); var listItem = new List<CListItem>(); foreach (var item in dictItems) { listItem.Add(new CListItem(item.Key, item.Value)); } listItem.Insert(0, new CListItem("顶级目录项", "-1")); this.ParentTypeList = listItem; this._isInitialized = true; }
我们在构建树形列表的时候,绑定的是一个过滤的列表对象,需要根据实际情况进行改变即可。
/// <summary> /// 触发处理命令 /// </summary> [RelayCommand] private async Task GetTree() { var treeeNodeList = new List<OuNodeInfo>(); var list = await SecurityHelper.GetMyTopGroup(App.ViewModel!.UserInfo); foreach (var groupInfo in list) { if (groupInfo != null && !groupInfo.IsDeleted) { var node = new OuNodeInfo(groupInfo); var sublist = await BLLFactory<IOuService>.Instance.GetTreeByID(groupInfo.Id); node.Children = sublist; treeeNodeList.Add(node); } } this.TreeItems = treeeNodeList; this.FilteredTreeItems = new List<OuNodeInfo>(this.TreeItems); }
界面的Xaml代码如下所示。
<hc:SearchBar Margin="0,10,10,0" hc:InfoElement.Placeholder="输入内容过滤" hc:InfoElement.ShowClearButton="True" IsRealTime="True" SearchStarted="SearchBar_OnSearchStarted" Style="{StaticResource SearchBarPlus}" />
如果在查询框里面输入内容,可以进行树列表的过滤,如下效果所示。
在输入内容的时候,我们触发一个事件SearchBar_OnSearchStarted 进行查询处理,根据查询的内容进行匹配处理。
/// <summary> /// 过滤查询事件 /// </summary> private void SearchBar_OnSearchStarted(object sender, HandyControl.Data.FunctionEventArgs<string> e) { this.ViewModel.SearchKey = e.Info; if (e.Info.IsNullOrEmpty()) { this.ViewModel.FilteredTreeItems = this.ViewModel.TreeItems; } else { this.ViewModel.FilteredTreeItems = FindNodes(this.ViewModel.TreeItems!, e.Info); } } /// <summary> /// 递归查询嵌套节点的内容,根据规则进行匹配 /// </summary> /// <typeparam name="T">数据类型</typeparam> /// <param name="nodes">节点集合</param> /// <param name="name">节点名称</param> /// <returns></returns> public List<T> FindNodes<T>(IEnumerable<T> nodes, string name) where T : OuNodeInfo { var result = new List<T>(); foreach (var node in nodes) { if (node.Name.Contains(name, StringComparison.OrdinalIgnoreCase)) { result.Add(node); } foreach (var childCategory in FindNodes(node.Children, name)) { result.Add((T)childCategory); } } return result; }
这样就可以实现数据的查询过滤,实际规则可以自己根据需要进行调整。
在我们设计软件的很多地方,都看到需要对表格数据进行导入和导出的操作,主要是方便客户进行快速的数据处理和分享的功能,本篇随笔介绍基于WPF实现DataGrid数据的导入和导出操作。