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" ItemsSource="{Binding TreeSource}"
              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();
        }
    }


    /// <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("
------------------------------------------------------");
        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"/>

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;
}
原文地址:https://www.cnblogs.com/wesson2019-blog/p/15161526.html