这周,来实现某个网友提的一个需求,查询节点,对于这个需求,我首先想到的界面是一个textbox和treeview的组合,关于查询,我想到的就是遍历树嘛,不过遍历树的方法有很多,递归的、非递归的、前、中、后等等,后面我想了想,我在广度优先和深度优先中,选择了广度优先中的非递归,当然,你也可以用其他的,这里我只想挑选一个好理解的方法来实现。
分析完了,那么,写代码吧,老规矩,还是先看总体项目的结构(看上去内容很多,其实大部分是后续讲的目录树拖动,今天才把demo弄好)
这次我采用了一些设计模式来实现,所以跟之前的项目有点差别,创建类库后,定义一个接口
public interface ITreeViewInterface
{
void Add(object obj);
void Delete(object obj);
void Update(string str);
void Search(string str);
void Select(object obj);
}
下一步是界面的搭建,如图,三行的简单布局,一个按钮,一个textbox和一个TreeView的组合
继续,开始创建对象,先来创建一个基类,如图(BindableObject,我就不介绍了,我这给出代码,不懂的去看我之前的)
public class BindableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
public void OnPropertyChanged([CallerMemberName]string propertyName = null)
{
PropertyChanged?.Invoke( this, new PropertyChangedEventArgs( propertyName ) );
}
}
public abstract class BaseModel : BindableObject
{
public abstract Guid Id { get; set; }
public abstract string Name { get; set; }
}
接下来让目录树对象继承基类,如图(只截了部分,太长了)
public class TreeViewModel : BaseModel
{
private Guid id;
public override Guid Id
{
get => id;
set
{
id = value;
OnPropertyChanged();
}
}
private string name;
public override string Name
{
get => name;
set
{
name = value;
OnPropertyChanged();
}
}
private Visibility isTextBoxVisibility = Visibility.Hidden;
/// <summary>
/// 右键显示隐藏
/// </summary>
public Visibility IsTextBoxVisibility
{
get => isTextBoxVisibility;
set
{
isTextBoxVisibility = value;
OnPropertyChanged();
}
}
private Visibility isNodeVisibility = Visibility.Visible;
/// <summary>
/// 查询后节点显示隐藏
/// </summary>
public Visibility IsNodeVisibility
{
get => isNodeVisibility;
set
{
isNodeVisibility = value;
OnPropertyChanged();
}
}
public TreeViewModel Parent { get; set; }
public ObservableCollection<TreeViewModel> Children { get; set; } = new ObservableCollection<TreeViewModel>();
public static TreeViewModel CreateModel()
{
TreeViewModel tree = new TreeViewModel()
{
Name="哈哈哈"
};
for (int i = 0; i < 3; i++)
{
TreeViewModel treeViewModel = new TreeViewModel()
{
Id = Guid.NewGuid(),
Name = $"{i}",
};
for (int j = 0; j < 5; j++)
{
treeViewModel.Children.Add(new TreeViewModel()
{
Id = Guid.NewGuid(),
Name = $"{i}_{j}",
Parent = treeViewModel,
});
}
tree.Children.Add(treeViewModel);
}
return tree;
}
我把初始化树的方法放在了目录树类里,让它本身有个方法自我初始化。
进军ViewModel,如图,拿到我们刚才定义的接口, 分别创建增、删、改、查已经选择节点的命令和方法(选择的方法主要用于右键重命名)(DelegateCommand不多说了)
public class MainViewModel
{
private readonly ITreeViewInterface @interface;
#region 命令
public DelegateCommand<BaseModel> AddCommand { get; set; }
public DelegateCommand<BaseModel> DeleteCommand { get; set; }
public DelegateCommand<string> UpdateCommand { get; set; }
public DelegateCommand<string> SearchCommand { get; set; }
public DelegateCommand<BaseModel> SelectCommand { get; set; }
#endregion
public MainViewModel(ITreeViewInterface @interface)
{
this.@interface = @interface;
AddCommand = new DelegateCommand<BaseModel>(Add);
DeleteCommand = new DelegateCommand<BaseModel>(Delete);
UpdateCommand = new DelegateCommand<string>(Update);
SearchCommand = new DelegateCommand<string>(Search);
SelectCommand = new DelegateCommand<BaseModel>(Select);
}
private void Add(BaseModel obj)
{
@interface.Add(obj);
}
private void Delete(BaseModel obj)
{
@interface.Delete(obj);
}
private void Update(string str)
{
@interface.Update(str);
}
private void Search(string str)
{
@interface.Search(str);
}
private void Select(BaseModel obj)
{
@interface.Select(obj);
}
}
然后,创建一个管理者——TreeViewManager,让它继承接口,如图
public class TreeViewManager : ITreeViewInterface
{
private static TreeViewModel _currentTree;
/// <summary>
/// 目录树
/// </summary>
public static TreeViewModel CurrentTree
{
get => _currentTree;
set
{
_currentTree = value;
OnCurrentProjectChanged();
}
}
public static event EventHandler CurrentProjectChanged;
/// <summary>
/// 通知更新
/// </summary>
public static void OnCurrentProjectChanged()
{
CurrentProjectChanged?.Invoke(CurrentTree, new EventArgs());
}
private TreeViewModel searchModel;
/// <summary>
/// 初始化
/// </summary>
public static void CreateTree()
{
CurrentTree = TreeViewModel.CreateModel();
}
/// <summary>
/// 添加
/// </summary>
/// <param name="obj"></param>
public void Add(object obj)
{
if (obj == null)
{
AddRoot();
return;
}
GetTreeModel(obj);
AddChild();
}
/// <summary>
/// 删除
/// </summary>
/// <param name="obj"></param>
public void Delete(object obj)
{
GetTreeModel(obj);
if (searchModel.Parent == null)
{
CurrentTree.Children.Remove(searchModel);
}
else
{
searchModel.Parent.Children.Remove(searchModel);
}
}
/// <summary>
/// 更新
/// </summary>
/// <param name="str"></param>
public void Update(string str)
{
searchModel.Name = str;
searchModel.IsTextBoxVisibility = System.Windows.Visibility.Hidden;
}
/// <summary>
/// 查询
/// </summary>
/// <param name="str"></param>
public void Search(string str)
{
if (str == "" || str == null)
{
CurrentTree.Children.ToList().ForEach(x =>
{
x.BFSTraverseTree((item) =>
{
item.IsNodeVisibility = System.Windows.Visibility.Visible;
});
});
return;
}
CurrentTree.Children.ToList().ForEach(x =>
{
x.BFSTraverseTree((item) =>
{
// 中文包含
if (item.Name.Contains(str))
{
ShowSearchResult(item);
}
else
{
item.IsNodeVisibility = System.Windows.Visibility.Collapsed;
}
});
});
}
/// <summary>
/// 右键重命名显示选择的的节点
/// </summary>
/// <param name="obj"></param>
public void Select(object obj)
{
GetTreeModel(obj);
searchModel.IsTextBoxVisibility = System.Windows.Visibility.Visible;
}
/// <summary>
/// 添加根节点
/// </summary>
private void AddRoot()
{
CurrentTree.Children.Add(new TreeViewModel()
{
Id = Guid.NewGuid(),
Name = $"{CurrentTree.Children.Count}"
});
}
/// <summary>
/// 添加子节点
/// </summary>
private void AddChild()
{
searchModel.Children.Add(new TreeViewModel()
{
Id = Guid.NewGuid(),
Name = $"{searchModel.Name}_{searchModel.Children.Count}",
Parent = searchModel
});
}
/// <summary>
/// 递归变量目录树
/// </summary>
/// <param name="model"></param>
/// <param name="guid"></param>
/// <returns></returns>
private TreeViewModel TraversalTree(TreeViewModel model, Guid guid)
{
foreach (var item in model.Children)
{
if (item.Id == guid)
{
searchModel = item;
return searchModel;
}
TraversalTree(item, guid);
}
return null;
}
/// <summary>
/// 获取选择的节点
/// </summary>
/// <param name="obj"></param>
private void GetTreeModel(object obj)
{
var baseResult = obj as BaseModel;
TraversalTree(CurrentTree, baseResult.Id);
}
/// <summary>
/// 显示查询的结果
/// </summary>
/// <param name="model"></param>
private void ShowSearchResult(TreeViewModel model)
{
if (model.Parent != null)
{
model.Parent.IsNodeVisibility = System.Windows.Visibility.Visible;
model.IsNodeVisibility = System.Windows.Visibility.Visible;
ShowSearchResult(model.Parent);
}
else
{
model.IsNodeVisibility = System.Windows.Visibility.Visible;
}
}
}
接着为treeviewmodel类创建一个扩展方法——BFSTraverseTree,如图
public static class TreeViewCommon
{
/// <summary>
/// 非递归广度优先遍历树
/// </summary>
/// <param name="item"></param>
/// <param name="action"></param>
public static void BFSTraverseTree(this TreeViewModel item, Action<TreeViewModel> action)
{
Queue<TreeViewModel> queue = new Queue<TreeViewModel>();
queue.Enqueue(item);
while (queue.Count > 0)
{
var curItem = queue.Dequeue();
action(curItem);
foreach (var subItem in curItem.Children)
{
queue.Enqueue(subItem);
}
}
}
}
关于Queue,官方给了很好的介绍,这里我就大致说下它的作用就是把目录树放到等待队列里面,当我们查询的时候,会直接在这个队列里查询(Queue),
接着,去完善viewmodel,添加如下代码
public TreeViewModel TreeModel => TreeViewManager.CurrentTree;
TreeViewManager.CreateTree();
那么,开始绑定界面把,如图,在界面的cs文件中,绑定数据源,用依赖注入的方法
<Window.Resources>
<ContextMenu x:Key="menu">
<MenuItem
Command="{Binding DataContext.AddCommand, RelativeSource={RelativeSource AncestorType=Window, Mode=FindAncestor}}"
CommandParameter="{Binding}"
Header="添加子节点" />
<MenuItem
Command="{Binding DataContext.DeleteCommand, RelativeSource={RelativeSource AncestorType=Window, Mode=FindAncestor}}"
CommandParameter="{Binding}"
Header="删除节点" />
<MenuItem
Command="{Binding DataContext.SelectCommand, RelativeSource={RelativeSource AncestorType=Window, Mode=FindAncestor}}"
CommandParameter="{Binding}"
Header="修改节点" />
</ContextMenu>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="40" />
<RowDefinition />
</Grid.RowDefinitions>
<StackPanel Margin="5" Orientation="Horizontal">
<Button
Width="100"
Height="30"
Command="{Binding AddCommand}"
Content="添加" />
</StackPanel>
<TextBox
x:Name="search"
Grid.Row="1"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center">
<i:Interaction.Triggers>
<i:EventTrigger EventName="TextChanged">
<i:InvokeCommandAction Command="{Binding DataContext.SearchCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}" CommandParameter="{Binding ElementName=search, Path=Text}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</TextBox>
<TreeView
x:Name="treeView"
Grid.Row="2"
AllowDrop="True"
ItemsSource="{Binding TreeModel.Children}">
<TreeView.Resources>
<Style TargetType="TreeViewItem">
<Setter Property="ContextMenu" Value="{StaticResource menu}" />
<Setter Property="Visibility" Value="{Binding IsNodeVisibility}" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="SkyBlue" />
</Trigger>
</Style.Triggers>
</Style>
</TreeView.Resources>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<Grid>
<TextBox
x:Name="textBox"
Text="{Binding Name}"
Visibility="{Binding IsTextBoxVisibility}">
<TextBox.InputBindings>
<KeyBinding
Key="Enter"
Command="{Binding DataContext.UpdateCommand, RelativeSource={RelativeSource AncestorType=Window, Mode=FindAncestor}}"
CommandParameter="{Binding ElementName=textBox, Path=Text}" />
</TextBox.InputBindings>
</TextBox>
<TextBlock Text="{Binding Name}" />
</Grid>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Grid>
this.DataContext = new MainViewModel(new TreeViewManager());
运行测试,如图
右侧修改发现显示不清楚,是因为同时显示了textbox和textblock,所以它俩只能显示一个,添加一个转换器,如图,
public class VisibilityToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if ((Visibility)value == Visibility.Visible)
{
return Visibility.Collapsed;
}
return Visibility.Visible;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return null;
}
}
在界面添加引用后,在textblock添加如图代码
再次运行测试,
没有问题了。
讨论
这次的案例,因为只是查询,没有要求怎么查询,所有并没有添加规则,有就有,没有就没有,这是本次案例的规则。
在编写的过程中,我过用不同的方式查询,采取什么方式,看自己对那种方式更好理解吧,
这次是把之前的增删改做了一次优化,这次的代码更加通俗易懂
代码地址:https://github.com/TQtong/TreeView.git
结束
好了,这次就到这了,欢迎各位批评指正,谢谢啦