【WPF】自定义控件:TreeViewEditControl-可同层级增删排序的目录树
可以在同一层级内增删排序节点的目录树实现,效果如下:
父节点可以右击新增子节点,新增数量可选;
子节点可以右击删除自身,可以在上方/下方插入同层级节点,可以上移或下移;
排序的部分,需要同时修改数据源列表的顺序。
数据
目录树为三层级,根节点、菜单节点、详情节点;
根节点无右击事件
菜单节点右击事件为新增子节点
详情节点右击为插入、排序和删除。
//1、数据源结构定义
public class DataSource : DataSourceItem
{
public List<DataSourceItem> Children { get; set; }
}
public class DataSourceItem
{
public Guid UniqueId { get; set; }
public string Name { get; set; }
}
//一些假数据
DataSource sys = new DataSource
{
UniqueId = Guid.NewGuid()
};
DataSource Node1 = new DataSource
{
UniqueId = Guid.NewGuid(),
Name = "Node1",
Children = new List<DataSourceItem>() {
new DataSourceItem{ UniqueId = Guid.NewGuid(),Name = "node" },
new DataSourceItem{ UniqueId = Guid.NewGuid(),Name = "node" },
}
};
//2、通用节点结构定义
public class Node
{
public Guid UniqueId { get; set; }
public string Name { get; set; }
public Guid ParentId { get; set; }
public Level Level { get; set; }
}
public enum Level
{
root,
menu,
detail
}
//3、数据源转通用节点字典
private Dictionary<DataSource, Node> nodeList = new Dictionary<DataSource, Node>();
//根节点
nodeList.Add(sys, new Node {
UniqueId = sys.UniqueId,
Name = "Sys",
ParentId = sys.UniqueId,
Level = Level.root
});
internal Node GetRoot()
{
return nodeList[sys];
}
//菜单节点
var node1 = GetNode(Node1, sys.UniqueId);
nodeList.Add(node1.Key, node1.Value);
internal Node GetMenuNodeList()
{
return from a in nodeList
where a.Value.ParentId == sys.UniqueId
&& a.Value.UniqueId != sys.UniqueId
select a.Value;
}
private KeyValuePair<T, Node> GetNode<T>(T o, Guid parentid, Level level = Level.menu)
{
return new KeyValuePair<T, Node>(o, new Node {
UniqueId = (Guid)o.GetType().GetProperty("UniqueId").GetValue(o),
Name = o.GetType().GetProperty("Name").GetValue(o).ToString(),
ParentId = parentid,
Level = level });
}
//详情节点
private List<Node> GetDetailNodeList(DataSource parent, Guid parentid)
{
List<Node> res = new List<Node>();
var list = parent.Children;
foreach (var o in list)
{
var node = GetNode(o, parentid, Level.detail);
res.Add(GetNode(o, parentid, Level.detail).Value);
}
return res;
}
//4、VM获取子节点(菜单层和详情层)
internal IEnumerable<Node> GetChildren(Guid parentId)
{
if (parentId == sys.UniqueId) return GetMenuNodeList();
foreach (var a in nodeList)
{
if (a.Value.UniqueId == parentId)
return GetDetailNodeList(a.Key, parentId);
}
return new List<Node>();
}
//5、增加节点
internal void AddNode(Node i, Guid parentId)
{
foreach (var a in nodeList)
{
if (a.Value.UniqueId == parentId)
{
a.Key.Children.Add(
new DataSourceItem() { UniqueId = i.UniqueId, Name = i.Name });
}
}
}
//6、删除节点
internal void RemoveNode(Guid nodeId, Guid parentId)
{
foreach (var a in nodeList)
{
if (a.Value.UniqueId == parentId)
{
var t = a.Key.Children.Find(_ => _.UniqueId == nodeId);
if (t != null) a.Key.Children.Remove(t);
}
}
}
//7、插入节点
internal void InsertNode(Node i, Guid tUid, Guid parentId, bool isBefore)
{
foreach (var a in nodeList)
{
if (a.Value.UniqueId == parentId)
{
var t = a.Key.Children.FindIndex(_ => _.UniqueId == tUid);
if (t != -1)
a.Key.Children.Insert(isBefore ? t : (t + 1),
new DataSourceItem { UniqueId = i.UniqueId, Name = i.Name });
}
}
}
//8、交换节点顺序
internal void SwapNode(Guid tUid, Guid parentId, bool isBefore)
{
foreach (var a in nodeList)
{
if (a.Value.UniqueId == parentId)
{
var t = a.Key.Children.FindIndex(_ => _.UniqueId == tUid);
if (t != -1)
{
if (isBefore && t == 0) return;
if (!isBefore && t == (a.Key.Children.Count - 1)) return;
int swapId = isBefore ? (t - 1) : (t + 1);
var temp = a.Key.Children[swapId];
a.Key.Children[swapId] = a.Key.Children[t];
a.Key.Children[t] = temp;
}
}
}
}
页面
View
其中涉及
<ContextMenu>控件绑定点击事件的实现方式
利用ItemTemplateSelector选择器实现目录树不同层级不同UI的方法
<UserControl x:Class="bueatifulApp.Components.TreeViewWithEdit.View.TreeViewWithEdit"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:bueatifulApp.Components.TreeViewWithEdit.View"
xmlns:vm="clr-namespace:bueatifulApp.Components.TreeViewWithEdit.ViewModel"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:b ="clr-namespace:bueatifulApp.Components.TreeViewWithEdit.Behavior"
xmlns:LocalTmeplate='clr-namespace:bueatifulApp.Components.TreeViewWithEdit.DataTemplates'
mc:Ignorable="d" x:Name="this">
<UserControl.Resources>
<vm:BindingProxy x:Key="Proxy" Data="{Binding}" />
<LocalTmeplate:ContextMenuDataTemplateSelector x:Key="ContextMenuDataTemplateSelector"/>
<ContextMenu x:Key="Tree_menuNodeMenu">
<MenuItem Name="Add" Command="{Binding Source={StaticResource Proxy}, Path=Data.AddNodeCommand}" CommandParameter="{Binding}">
<MenuItem.Header>
<StackPanel Orientation="Horizontal">
<Label Content="新增"/>
<TextBox Width="20" Text="{Binding Source={StaticResource Proxy}, Path=Data.AddCount}"
Height="20" HorizontalContentAlignment="Center" VerticalContentAlignment="Center"/>
<Label Content="个子节点"/>
</StackPanel>
</MenuItem.Header>
</MenuItem>
</ContextMenu>
<ContextMenu x:Key="Tree_detailNodeMenu">
<MenuItem Header="删除" Name="Delete_self"
Command="{Binding Source={StaticResource Proxy}, Path=Data.DeleteNodeCommand}" CommandParameter="{Binding}"/>
<Separator/>
<MenuItem Header="在上方插入" Name="Insert_up"
Command="{Binding Source={StaticResource Proxy}, Path=Data.InsertBeforeNodeCommand}" CommandParameter="{Binding}"/>
<MenuItem Header="在下方插入" Name="Insert_down"
Command="{Binding Source={StaticResource Proxy}, Path=Data.InsertAfterNodeCommand}" CommandParameter="{Binding}"/>
<Separator/>
<MenuItem Header="上移" Name="swap_up"
Command="{Binding Source={StaticResource Proxy}, Path=Data.SwapBeforeNodeCommand}" CommandParameter="{Binding}"/>
<MenuItem Header="下移" Name="swap_down"
Command="{Binding Source={StaticResource Proxy}, Path=Data.SwapAfterNodeCommand}" CommandParameter="{Binding}"/>
</ContextMenu>
<HierarchicalDataTemplate x:Key="menuNodeTemplate" ItemsSource="{Binding Children}" DataType="{x:Type vm:MenuOrgNodeViewModel}">
<StackPanel Orientation="Horizontal" ContextMenu="{StaticResource Tree_menuNodeMenu}">
<Label Content="{Binding Name}"/>
</StackPanel>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate x:Key="detailNodeTemplate" ItemsSource="{Binding Children}" DataType="{x:Type vm:DetailOrgNodeViewModel}">
<StackPanel Orientation="Horizontal" ContextMenu="{StaticResource Tree_detailNodeMenu}">
<Label >
<Label.Content>
<TextBlock Text="{Binding Index,StringFormat={}[{0}号]}"/>
</Label.Content>
</Label>
<Label Content="{Binding Name}"/>
</StackPanel>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate x:Key="rootNodeTemplate" ItemsSource="{Binding Children}" DataType="{x:Type vm:RootOrgNodeViewModel}">
<StackPanel Orientation="Horizontal">
<Label Content="{Binding Name}"/>
</StackPanel>
</HierarchicalDataTemplate>
</UserControl.Resources>
<StackPanel >
<TreeView Height="365" ItemsSource="{Binding Root}" ItemTemplateSelector="{StaticResource ContextMenuDataTemplateSelector}"/>
</StackPanel>
</UserControl>
选择器定义
public class ContextMenuDataTemplateSelector : DataTemplateSelector
{
public override System.Windows.DataTemplate SelectTemplate(object item, System.Windows.DependencyObject container)
{
FrameworkElement element = container as FrameworkElement;
DataTemplate template = null;
if (item is MenuOrgNodeViewModel)
{
template = element.FindResource("menuNodeTemplate") as HierarchicalDataTemplate;
}
else if (item is RootOrgNodeViewModel)
{
template = element.FindResource("rootNodeTemplate") as HierarchicalDataTemplate;
}
else if (item is DetailOrgNodeViewModel)
{
template = element.FindResource("detailNodeTemplate") as DataTemplate;
}
return template;
}
}
<ContextMenu>控件绑定点击事件的代理器定义
public class BindingProxy : Freezable
{
protected override Freezable CreateInstanceCore()
{
return new BindingProxy();
}
public object Data
{
get { return (object)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
}
ViewModel
每个节点对应的VM,处理基本数据
public class NodeViewModel : ObservableObject
{
private Guid uniqueId;
private string name;
private Level level;
private bool isSelected;
public Guid UniqueId { get => uniqueId; set => uniqueId = value; }
public string Name { get => name; set => name = value; }
public Level Level { get => level; set => level = value; }
public bool IsSelected { get => isSelected; set => OnPropertyChanged(ref isSelected, value); }
internal NodeViewModel(Node node)
{
this.UniqueId = node.UniqueId;
this.Name = node.Name;
this.level = node.Level;
}
}
定义每个节点的父子关系和节点操作,和层级
每次增删排序后应当重新获取子节点,才能更新到页面上
public class MenuOrgNodeViewModel : OrgNodeViewModel
{
internal MenuOrgNodeViewModel(Node i, OrgNodeViewModel parent = null) : base(i, parent)
{
}
}
public class RootOrgNodeViewModel : OrgNodeViewModel
{
internal RootOrgNodeViewModel(Node i, OrgNodeViewModel parent = null) : base(i, parent)
{
}
}
public class DetailOrgNodeViewModel : OrgNodeViewModel
{
internal DetailOrgNodeViewModel(Node i, OrgNodeViewModel parent = null) : base(i, parent)
{
}
}
public class OrgNodeViewModel : NodeViewModel
{
internal OrgNodeViewModel(Node i, OrgNodeViewModel parent = null)
: base(i)
{
this.parent = parent;
}
private ObservableCollection<OrgNodeViewModel> children;
private OrgNodeViewModel parent;
public ObservableCollection<OrgNodeViewModel> Children
{
get
{
if (children == null)
return GetChildren();
return children;
}
set
{
OnPropertyChanged(ref children, value);
}
}
public int Index {
get {
return parent.Children.ToList()
.FindIndex(_ => _.UniqueId == this.UniqueId);
}
}
private ObservableCollection<OrgNodeViewModel> GetChildren()
{
children = new ObservableCollection<OrgNodeViewModel>();
foreach (Node i in TreeDataAccess.GetIns()
.GetChildren(this.UniqueId))
{
if (i.Level == Level.menu)
children.Add(new MenuOrgNodeViewModel(i, this));
else if (i.Level == Level.detail)
children.Add(new DetailOrgNodeViewModel(i, this));
}
return children;
}
private void RefreshChildren()
{
Children = GetChildren();
}
public void AddChild(int count = 1)
{
for (int i = 0; i < count; i++)
{
TreeDataAccess.GetIns().AddNode(new Node {
UniqueId = Guid.NewGuid(),
Name = "add" + i,
Level = Level.detail ,
ParentId = this.UniqueId}, this.UniqueId);
}
RefreshChildren();
}
public void DeleteSelf() {
TreeDataAccess.GetIns()
.RemoveNode(this.UniqueId,this.parent.UniqueId);
this.parent.RefreshChildren();
}
public void InsertNode(bool isBefore) {
TreeDataAccess.GetIns().InsertNode(new Node {
UniqueId = Guid.NewGuid(),
Name = "INSERT",
Level = Level.detail,
ParentId = this.parent.UniqueId },
this.UniqueId,
this.parent.UniqueId,
isBefore);
this.parent.RefreshChildren();
}
public void SwapNode(bool isBefore)
{
TreeDataAccess.GetIns().SwapNode(
this.UniqueId,
this.parent.UniqueId,
isBefore);
this.parent.RefreshChildren();
}
}
完整目录树的VM
public class TreeViewWithEditViewModel : ObservableObject
{
private List<OrgNodeViewModel> root;
public List<OrgNodeViewModel> Root
{
get
{
if (root == null)
{
root = new List<OrgNodeViewModel>
{
new RootOrgNodeViewModel(TreeDataAccess.GetIns().GetRoot())
};
}
return root;
}
set => OnPropertyChanged(ref root, value);
}
private int addCount = 1;
public int AddCount
{
get { return addCount; }
set { OnPropertyChanged(ref addCount, value); }
}
public TreeViewWithEditViewModel()
{
AddNodeCommand = new RelayCommand((o) =>
{
(o as OrgNodeViewModel).AddChild(AddCount);
AddCount = 1;
});
DeleteNodeCommand = new RelayCommand((o) =>
{
(o as OrgNodeViewModel).DeleteSelf();
});
InsertBeforeNodeCommand = new RelayCommand((o) =>
{
(o as OrgNodeViewModel).InsertNode(true);
});
InsertAfterNodeCommand = new RelayCommand((o) =>
{
(o as OrgNodeViewModel).InsertNode(false);
});
SwapBeforeNodeCommand = new RelayCommand((o) =>
{
(o as OrgNodeViewModel).SwapNode(true);
});
SwapAfterNodeCommand = new RelayCommand((o) =>
{
(o as OrgNodeViewModel).SwapNode(false);
});
}
public RelayCommand AddNodeCommand { get; }
public RelayCommand DeleteNodeCommand { get; }
public RelayCommand InsertBeforeNodeCommand { get; }
public RelayCommand InsertAfterNodeCommand { get; }
public RelayCommand SwapBeforeNodeCommand { get; }
public RelayCommand SwapAfterNodeCommand { get; }
public RelayCommand SwapNodeCommand { get; }
}