這周,來實作某個網友提的一個需求,查詢節點,對于這個需求,我首先想到的界面是一個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)
{
[email protected] = @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
結束
好了,這次就到這了,歡迎各位批評指正,謝謝啦