CommonTree
树结构的数据显示,
满足一般的数据加载、查询、刷新、勾选并获取勾选项
xaml
<UserControl.Resources>
<BooleanToVisibilityConverter x:Key="Boolean2VisibilityConverter"/>
<HierarchicalDataTemplate x:Key="ItemNode" ItemsSource="{Binding Children,Mode=TwoWay}">
<Grid Background="Transparent">
<StackPanel MinHeight="25" Orientation="Horizontal" Background="Transparent"
HorizontalAlignment="Left" >
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="25"></ColumnDefinition>
<ColumnDefinition Width="auto"></ColumnDefinition>
<ColumnDefinition Width="auto"></ColumnDefinition>
</Grid.ColumnDefinitions>
<CheckBox Grid.Column="0" IsChecked="{Binding IsCheck, Mode=TwoWay}" Tag="{Binding Node}"
PreviewMouseUp="CheckBox_PreviewMouseUp">
</CheckBox>
<TextBlock Grid.Column="1" Text="{Binding Name}" VerticalAlignment="Center" HorizontalAlignment="Center" Margin="3 0" />
<TextBlock Grid.Column="2" Text="{Binding Remark}" VerticalAlignment="Center" HorizontalAlignment="Center" Margin="3 0" />
</Grid>
</StackPanel>
</Grid>
</HierarchicalDataTemplate>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" KeyDown="TextBoxSearchContent_KeyDown" Text="{Binding SearchContent, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"/>
<Button Grid.Column="1" Style="{DynamicResource MahApps.Styles.Button.Circle}" Content="{iconPacks:ForkAwesome Kind=Search}" ToolTip="查询" Width="30" Height="30" Margin="5" Click="ButtonSearch_Click"></Button>
<Button Grid.Column="2" Style="{DynamicResource MahApps.Styles.Button.Circle}" Content="{iconPacks:ForkAwesome Kind=Refresh}" ToolTip="刷新" Width="30" Height="30" Margin="5" Click="ButtonRefresh_Click"></Button>
</Grid>
<Grid Grid.Row="1" Visibility="{Binding SearchContentVisible, Converter={StaticResource Boolean2VisibilityConverter}, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="1" Grid.Column="0" x:Name="lblSearch" />
<Button Grid.Row="1" Grid.Column="1" Style="{DynamicResource ButtonICOStyle}" Content="{iconPacks:Modern Kind=ArrowDown}" Width="30" Height="30" Click="ButtonNext_Click" VerticalAlignment="Center" />
<Button Grid.Row="1" Grid.Column="2" Style="{DynamicResource ButtonICOStyle}" Content="{iconPacks:Modern Kind=ArrowUp}" Width="30" Height="30" Click="ButtonPre_Click" VerticalAlignment="Center" />
</Grid>
<TreeView Grid.Row="2" Name="treeFrameInfo"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
BorderThickness="0"
ItemTemplate="{DynamicResource ItemNode}"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode ="Recycling">
<TreeView.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel/>
</ItemsPanelTemplate>
</TreeView.ItemsPanel>
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
</Grid>
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
public partial class CommonTree : UserControl
{
private FrameTree _treeRoot;
private int _searchIndex = 0;
/// <summary>
/// 查询到的项目列表
/// </summary>
private List<FrameTree> _searchListItems;
/// <summary>
/// 查询到的项目结点
/// </summary>
private List<TreeViewItem> _searchTreeViewItems;
private TreeViewItemHelper _helper = null;
/// <summary>
/// 已经勾选的项目
/// </summary>
public List<FrameTree> SelectItems { get; set; }
/// <summary>
/// 待查询内容
/// </summary>
public string SearchContent
{
get { return (string)GetValue(SearchContentProperty); }
set { SetValue(SearchContentProperty, value); }
}
public static readonly DependencyProperty SearchContentProperty =
DependencyProperty.Register("SearchContent", typeof(string), typeof(CommonTree), new PropertyMetadata(null));
/// <summary>
/// 查询统计结果可见性
/// </summary>
public bool SearchContentVisible
{
get { return (bool)GetValue(SearchContentVisibleProperty); }
set { SetValue(SearchContentVisibleProperty, value); }
}
public static readonly DependencyProperty SearchContentVisibleProperty =
DependencyProperty.Register("SearchContentVisible", typeof(bool), typeof(CommonTree), new PropertyMetadata(false));
/// <summary>
/// 树的数据源
/// </summary>
public List<FrameTree> DataSource
{
get => (List<FrameTree>)GetValue(DataSourceProperty);
set => SetValue(DataSourceProperty, value);
}
public static readonly DependencyProperty DataSourceProperty =
DependencyProperty.Register("DataSource", typeof(List<FrameTree>), typeof(CommonTree), new PropertyMetadata(null, OnDataSourceChanged));
private static void OnDataSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
CommonTree ctl = (CommonTree)d;
ctl.OnDataSourceChanged(e.NewValue);
}
private void OnDataSourceChanged(object newValue)
{
if (newValue is List<FrameTree> list && list.Count > 0)
{
_treeRoot = list[0];
TreeClear();
treeFrameInfo.ItemsSource = list;
}
}
/// <summary>
/// 刷新事件
/// </summary>
public event EventHandler TreeRefreshEvent;
/// <summary>
/// 结点勾选通知事件
/// </summary>
public event EventHandler TreeCheckChangedEvent;
public CommonTree()
{
InitializeComponent();
if (System.ComponentModel.DesignerProperties.GetIsInDesignMode(this))
{
return;
}
InitOther();
}
// 参数初始化
private void InitOther()
{
SelectItems = new List<FrameTree>();
_helper = new TreeViewItemHelper();
_searchListItems = new List<FrameTree>();
_searchTreeViewItems = new List<TreeViewItem>();
}
/// <summary>
/// 清空树的勾选项
/// </summary>
public void TreeClear()
{
SelectItems.Clear();
RecursionCheckState(_treeRoot, false);
}
/// <summary>
/// 递归改变模型对象及子对象的勾选状态
/// </summary>
/// <param name="node">模型对象</param>
/// <param name="state">状态值</param>
private void RecursionCheckState(FrameTree node, bool state)
{
if (node == null)
{
return;
}
node.IsCheck = state;
// 递归
if (node.Children != null)
{
foreach (FrameTree item in node.Children)
{
RecursionCheckState(item, state);
}
}
}
// 文本框回车查询
private void TextBoxSearchContent_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == System.Windows.Input.Key.Enter)
{
ButtonSearch_Click(null, null);
}
}
// 查询
private void ButtonSearch_Click(object sender, RoutedEventArgs e)
{
try
{
if (string.IsNullOrWhiteSpace(SearchContent))
{
SearchContentVisible = false;
return;
}
SearchContentVisible = true;
_searchIndex = 0;
_searchListItems.Clear();
_searchTreeViewItems.Clear();
SearchFrameTree(_treeRoot, SearchContent, _searchListItems);
if (_searchListItems.Count > 0)
{
_searchTreeViewItems.AddRange(_helper.GetTreeViewItems(treeFrameInfo, _searchListItems));
if (_searchTreeViewItems.Count > 0)
{
var item = _searchTreeViewItems[0];
if (!_helper.IsExistVisible(item))
{
_ = _helper.PositionTreeViewItem(treeFrameInfo, _searchListItems[0]);
}
Position(item);
}
lblSearch.Text = $"当前【{_searchIndex + 1}】,共【{_searchTreeViewItems.Count}】项";
}
else
{
lblSearch.Text = $"当前【0】,共【0】项";
}
}
catch (Exception ex)
{
Console.WriteLine("搜索发生异常:" + ex.Message.ToString(), ex);
}
}
// 刷新
private void ButtonRefresh_Click(object sender, RoutedEventArgs e)
{
SearchContent = string.Empty;
SearchContentVisible = false;
SelectItems.Clear();
_helper.ScrollToTop(treeFrameInfo);
// 事件通知
TreeRefreshEvent?.Invoke(this, EventArgs.Empty);
}
// 下一个
private void ButtonNext_Click(object sender, RoutedEventArgs e)
{
_searchIndex++;
if (_searchIndex > _searchTreeViewItems.Count - 1)
{
_searchIndex = _searchTreeViewItems.Count - 1;
}
var item = _searchTreeViewItems[_searchIndex];
if (!_helper.IsExistVisible(item))
{
Console.WriteLine($"当前项已不可见!!!");
_searchTreeViewItems.Clear();
_searchTreeViewItems.AddRange(_helper.GetTreeViewItems(treeFrameInfo, _searchListItems));
}
else if (item.DataContext is FrameTree frame && !frame.Name.Contains(SearchContent))
{
Console.WriteLine($"{frame.Name}: {SearchContent} 跑偏了!!!");
_searchTreeViewItems.Clear();
_searchTreeViewItems.AddRange(_helper.GetTreeViewItems(treeFrameInfo, _searchListItems));
}
Position(_searchTreeViewItems[_searchIndex]);
SearchContentVisible = true;
lblSearch.Text = $"当前【{_searchIndex + 1}】,共【{_searchTreeViewItems.Count}】项";
}
// 上一个
private void ButtonPre_Click(object sender, RoutedEventArgs e)
{
_searchIndex--;
if (_searchIndex < 0)
{
_searchIndex = 0;
}
var item = _searchTreeViewItems[_searchIndex];
if (!_helper.IsExistVisible(_searchTreeViewItems[_searchIndex]))
{
Console.WriteLine($"当前项已不可见!!!");
_searchTreeViewItems.Clear();
_searchTreeViewItems = _helper.GetTreeViewItems(treeFrameInfo, _searchListItems);
}
else if (item.DataContext is FrameTree frame && !frame.Name.Contains(SearchContent))
{
Console.WriteLine($"{frame.Name}: {SearchContent} 跑偏了!!!");
_searchTreeViewItems.Clear();
_searchTreeViewItems.AddRange(_helper.GetTreeViewItems(treeFrameInfo, _searchListItems));
}
Position(_searchTreeViewItems[_searchIndex]);
SearchContentVisible = true;
lblSearch.Text = $"当前【{_searchIndex + 1}】,共【{_searchTreeViewItems.Count}】项";
}
/// <summary>
/// TreeViewItem定位
/// </summary>
/// <param name="item">TreeViewItem</param>
private void Position(TreeViewItem item)
{
if (item == null)
{
return;
}
#if DEBUG
Console.WriteLine("\r\n------------------------------------------------------");
var indx = _searchTreeViewItems.IndexOf(item);
var preIdx = indx - 1;
if (preIdx >= 0)
{
PrintItem(preIdx, _searchTreeViewItems[preIdx]);
}
Console.Write("√");
PrintItem(indx, item);
var nextIdx = indx + 1;
if (nextIdx < _searchTreeViewItems.Count)
{
PrintItem(nextIdx, _searchTreeViewItems[nextIdx]);
}
Console.WriteLine("------------------------------------------------------");
#endif
// 虚化造成效果不佳,有可能定位不精确,可以来回多切几次
//item.BringIntoView();
// item.UpdateLayout();
item.IsSelected = true;
item.IsExpanded = true;
_ = item.Focus();
}
private void PrintItem(int index, TreeViewItem item)
{
if (item.DataContext is FrameTree frame)
{
Console.WriteLine($"{index}. {frame.Name}");
}
else
{
Console.WriteLine($"{index}. {item.DataContext}");
}
}
// 勾选框点击之前的操作
private void CheckBox_PreviewMouseUp(object sender, MouseButtonEventArgs e)
{
if (sender is CheckBox ckb && ckb.Tag is FrameTree node)
{
// 点击之前的状态
if (ckb.IsChecked.HasValue && ckb.IsChecked.Value)
{
ChangeCheckState(node, false);
ckb.IsChecked = true;
}
else
{
ChangeCheckState(node, true);
ckb.IsChecked = false;
}
// 事件通知
TreeCheckChangedEvent?.Invoke(this, new TreeCheckBoxEventArgs { SelectItems = SelectItems });
}
}
private static int _count = 0;
/// <summary>
/// 改变模型对象及子对象的勾选状态
/// </summary>
/// <param name="node">模型对象</param>
/// <param name="state">状态值</param>
private void ChangeCheckState(FrameTree node, bool state)
{
if (node == null)
{
return;
}
// 状态变化了
if (node.IsCheck != state)
{
if (state)
{
_count++;
SelectItems.Add(node);
}
else
{
_count--;
_ = SelectItems.RemoveAll(x => x.Guid == node.Guid);
}
Console.WriteLine($"{_count}: {node.Name}:{node.IsCheck}==>{state}");
// 递归
if (node.Children != null)
{
foreach (FrameTree item in node.Children)
{
ChangeCheckState(item, state);
}
}
}
node.IsCheck = state;
}
/// <summary>
/// 查询满足条件的数据模型对象
/// </summary>
/// <param name="node">查询起始点</param>
/// <param name="content">查询内容</param>
/// <param name="rslt">对象集合</param>
private void SearchFrameTree(FrameTree node, string content, List<FrameTree> rslt)
{
if (node.Name.Contains(content))
{
rslt.Add(node);
}
foreach (FrameTree child in node.Children)
{
SearchFrameTree(child, content, rslt);
}
}
}
其中,结构树的实体FrameTree,树的帮助类TreeViewItemHelper可自己定义实现,
demo
<wesson:CommonTree x:Name="CommonTreeInfo" TreeRefreshEvent="CommonTreeInfo_TreeRefreshEvent"/>
DataBindAsync();
CommonTreeInfo.DataSource = TreeSource;
prism
xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
<wesson:CommonTree DataSource="{Binding TreeSource, Mode=TwoWay}" x:Name="CommonTreeInfo"
SearchContent="{Binding PropSetEntity.SearchContent, Mode=TwoWay}"
SearchContentVisible="{Binding PropSetEntity.SearchContentVisible, Mode=TwoWay}">
<b:Interaction.Triggers>
<b:EventTrigger EventName="TreeRefreshEvent">
<b:InvokeCommandAction Command="{Binding CommonTreeRefreshCommand}"/>
</b:EventTrigger>
<b:EventTrigger EventName="TreeCheckChangedEvent">
<b:InvokeCommandAction Command="{Binding CommonTreeCheckChangedCommand}" PassEventArgsToCommand="True"/>
</b:EventTrigger>
</b:Interaction.Triggers>
</wesson:CommonTree>
// view
private void ButtonClear_Click(object sender, System.Windows.RoutedEventArgs e)
{
CommonTreeInfo.TreeClear();
}
private void ButtonGetData_Click(object sender, System.Windows.RoutedEventArgs e)
{
var btn = (Button)sender;
btn.CommandParameter = CommonTreeInfo.SelectItems;
}
// viewmodel
private List<FrameTree> _treeSource;
/// <summary>
/// 可绑定树的数据列表
/// </summary>
public List<FrameTree> TreeSource
{
get => _treeSource;
set => SetProperty(ref _treeSource, value);
}
public DelegateCommand CommonTreeRefreshCommand { get; private set; }
public DelegateCommand<object> CommonTreeCheckChangedCommand { get; private set; }
public ctor()
{
TreeSource = new List<FrameTree>();
CommonTreeRefreshCommand = new DelegateCommand(CommonTree_Refresh);
CommonTreeCheckChangedCommand = new DelegateCommand<object>(CommonTree_CheckChanged);
}
private void CommonTree_CheckChanged(object parameter)
{
if (parameter is TreeCheckBoxEventArgs args)
{
// TODO
}
}
private async Task DataBindAsync()
{
var response = // 异步获取数据
if (response != null)
{
var root = new FrameTree();
root = GetTreeFrameChildren(response, root);
TreeSource.Clear();
TreeSource = new List<FrameTree> { root };
}
}
private FrameTree GetTreeFrameChildren(object dto, FrameTree parentNode)
{
var node = new FrameTree
{
Guid = dto.Guid,
Name = dto.Name,
Type = dto.Type,
Parent = parentNode,
OriginalSource = dto,
};
if (dto.Children == null)
{
return node;
}
foreach (var item in dto.Children.OrderBy(o => o.Name))
{
node.Children.Add(GetTreeFrameChildren(item, node));
}
return node;
}