[英]Is there any wrap object class, that you get from IEnumerable? (ItemsSource in TreeView)
我想在 WPF 中制作自己的 UserControl,基於 TreeView。 我的目標是這樣做,有機會從代碼中更改 SelectedItem。
在 MVVM 模式中,您可以在 TreeItemViewModel 中創建“IsSelected”屬性並在 ItemContainerStyle 中綁定“IsSelected”,如下所示:
XAML:
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding IsExpanded}"/>
<Setter Property="IsSelected" Value="{Binding IsSelected}"/>
</Style>
</TreeView.ItemContainerStyle>
樹項視圖模型:
private bool _isExpanded;
public bool IsExpanded { get { return _isExpanded; } set { _isExpanded = value; OnPropertyChanged("IsExpanded"); } }
private bool _isSelected;
public bool IsSelected { get { return _isESelected; } set { _isSelected = value; OnPropertyChanged("IsSelected"); } }
通過這樣做,您可以查看綁定到 ItemsSource 的所有 ObservableCollection<TreeItemViewModel>,找到所需的元素並為其父級更改“IsExpanded”和“IsSelected”。
我希望我的 UserControl 在其中包含所有這些綁定。 我的 UserControl 將繼承自 TreeView,並且我將創建自己的 MyItemsSource,它將采用 IEnumerable(就像 TreeView 中的原始 ItemsSource)。 在我看來,我計划的下一階段是將 IEnumerable 中的對象包裝在新的 class 中,它將具有另外兩個屬性:IsSelected 和 IsExpanded。 然后在我的 UserControl 中綁定這個屬性。
因此,在我未來的項目中,我希望不能添加這兩個屬性並從代碼中更改 SelectedItem。
如何將我從 IEnumerable 獲得的對象(不知道我得到的 class,因為它是 UserControl)包裝在具有兩個附加屬性的新 class 中?
用戶控制 class:
public partial class UserControl : TreeView
{
public UserControl()
{
InitializeComponent();
}
public System.Collections.IEnumerable MyItemsSource
{
set
{
ObservableCollection<UserControlTreeItemViewModel> ItemsSourceWrapped = new ObservableCollection<UserControlTreeItemViewModel>();
// wrap objects in cycle
foreach(var item in value)
{
ItemsSourceWrapped.Add(new UserControlTreeItemViewModel(item));
}
this.ItemsSource = ItemsSourceWrapped;
}
}
}
和 class 來包裝對象:
public class UserControlTreeItemViewModel : Object
{
public UserControlTreeItemViewModel(object i)
{
// How to write constructor to wrap object that you get?
}
public bool IsSelected { get; set; }
public bool IsExpanded { get; set; }
}
主要問題:有什么方法可以包裝從 IEnumerable 獲得的對象?
這是基本 ViewModel class,我用於在 TreeView 控件中顯示數據項。 它處理諸如子項收集、擴展、選擇、檢查(勾選框)之類的事情,包括更新父項和子項、禁用 state、延遲加載子項。
public class perTreeViewItemViewModelBase : perViewModelBase
{
// a dummy item used in lazy loading mode, ensuring that each node has at least one child so that the expand button is shown
private static perTreeViewItemViewModelBase LazyLoadingChildIndicator { get; }
= new perTreeViewItemViewModelBase { Caption = "Loading Data ..." };
private bool InLazyLoadingMode { get; set; }
private bool LazyLoadTriggered { get; set; }
private bool LazyLoadCompleted { get; set; }
private bool RequiresLazyLoad => InLazyLoadingMode && !LazyLoadTriggered;
// Has Children been overridden (e.g. to point at some private internal collection)
private bool LazyLoadChildrenOverridden => InLazyLoadingMode && !Equals(LazyLoadChildren, _childrenList);
private readonly perObservableCollection<perTreeViewItemViewModelBase> _childrenList
= new perObservableCollection<perTreeViewItemViewModelBase>();
/// <summary>
/// LazyLoadingChildIndicator ensures a visible expansion toggle button in lazy loading mode
/// </summary>
protected void SetLazyLoadingMode()
{
ClearChildren();
_childrenList.Add(LazyLoadingChildIndicator);
IsExpanded = false;
InLazyLoadingMode = true;
LazyLoadTriggered = false;
LazyLoadCompleted = false;
}
private string _caption;
public string Caption
{
get => _caption;
set => Set(nameof(Caption), ref _caption, value);
}
public void ClearChildren()
{
_childrenList.Clear();
}
/// <summary>
/// Add a new child item to this TreeView item
/// </summary>
/// <param name="child"></param>
public void AddChild(perTreeViewItemViewModelBase child)
{
if (LazyLoadChildrenOverridden)
{
throw new InvalidOperationException("Don't call AddChild for an item with LazyLoad mode set & LazyLoadChildren has been overridden");
}
if (_childrenList.Any() && _childrenList.First() == LazyLoadingChildIndicator)
{
_childrenList.Clear();
}
_childrenList.Add(child);
SetChildPropertiesFromParent(child);
}
protected void SetChildPropertiesFromParent(perTreeViewItemViewModelBase child)
{
child.Parent = this;
// if this node is checked then all new children added are set checked
if (IsChecked.GetValueOrDefault())
{
child.SetIsCheckedIncludingChildren(true);
}
ReCalculateNodeCheckState();
}
protected void ReCalculateNodeCheckState()
{
var item = this;
while (item != null)
{
if (item.Children.Any() && !Equals(item.Children.FirstOrDefault(), LazyLoadingChildIndicator))
{
var hasIndeterminateChild = item.Children.Any(c => c.IsEnabled && !c.IsChecked.HasValue);
if (hasIndeterminateChild)
{
item.SetIsCheckedThisItemOnly(null);
}
else
{
var hasSelectedChild = item.Children.Any(c => c.IsEnabled && c.IsChecked.GetValueOrDefault());
var hasUnselectedChild = item.Children.Any(c => c.IsEnabled && !c.IsChecked.GetValueOrDefault());
if (hasUnselectedChild && hasSelectedChild)
{
item.SetIsCheckedThisItemOnly(null);
}
else
{
item.SetIsCheckedThisItemOnly(hasSelectedChild);
}
}
}
item = item.Parent;
}
}
private void SetIsCheckedIncludingChildren(bool? value)
{
if (IsEnabled)
{
_isChecked = value;
RaisePropertyChanged(nameof(IsChecked));
foreach (var child in Children)
{
if (child.IsEnabled)
{
child.SetIsCheckedIncludingChildren(value);
}
}
}
}
private void SetIsCheckedThisItemOnly(bool? value)
{
_isChecked = value;
RaisePropertyChanged(nameof(IsChecked));
}
/// <summary>
/// Add multiple children to this TreeView item
/// </summary>
/// <param name="children"></param>
public void AddChildren(IEnumerable<perTreeViewItemViewModelBase> children)
{
foreach (var child in children)
{
AddChild(child);
}
}
/// <summary>
/// Remove a child item from this TreeView item
/// </summary>
public void RemoveChild(perTreeViewItemViewModelBase child)
{
_childrenList.Remove(child);
child.Parent = null;
ReCalculateNodeCheckState();
}
public perTreeViewItemViewModelBase Parent { get; private set; }
private bool? _isChecked = false;
public bool? IsChecked
{
get => _isChecked;
set
{
if (Set(nameof(IsChecked), ref _isChecked, value))
{
foreach (var child in Children)
{
if (child.IsEnabled)
{
child.SetIsCheckedIncludingChildren(value);
}
}
Parent?.ReCalculateNodeCheckState();
}
}
}
private bool _isExpanded;
public bool IsExpanded
{
get => _isExpanded;
set
{
if (Set(nameof(IsExpanded), ref _isExpanded, value) && value && RequiresLazyLoad)
{
TriggerLazyLoading();
}
}
}
private bool _isEnabled = true;
public bool IsEnabled
{
get => _isEnabled;
set => Set(nameof(IsEnabled), ref _isEnabled, value);
}
public void TriggerLazyLoading()
{
var unused = DoLazyLoadAsync();
}
private async Task DoLazyLoadAsync()
{
if (LazyLoadTriggered)
{
return;
}
LazyLoadTriggered = true;
var lazyChildrenResult = await LazyLoadFetchChildren()
.EvaluateFunctionAsync()
.ConfigureAwait(false);
LazyLoadCompleted = true;
if (lazyChildrenResult.IsCompletedOk)
{
var lazyChildren = lazyChildrenResult.Data;
foreach (var child in lazyChildren)
{
SetChildPropertiesFromParent(child);
}
// If LazyLoadChildren has been overridden then just refresh the check state (using the new children)
// and update the check state (in case any of the new children is already set as checked)
if (LazyLoadChildrenOverridden)
{
ReCalculateNodeCheckState();
}
else
{
AddChildren(lazyChildren); // otherwise add the new children to the base collection.
}
}
RefreshChildren();
}
/// <summary>
/// Get the children for this node, in Lazy-Loading Mode
/// </summary>
/// <returns></returns>
protected virtual Task<perTreeViewItemViewModelBase[]> LazyLoadFetchChildren()
{
return Task.FromResult(new perTreeViewItemViewModelBase[0]);
}
/// <summary>
/// Update the Children property
/// </summary>
public void RefreshChildren()
{
RaisePropertyChanged(nameof(Children));
}
/// <summary>
/// In LazyLoading Mode, the Children property can be set to something other than
/// the base _childrenList collection - e.g as the union ot two internal collections
/// </summary>
public IEnumerable<perTreeViewItemViewModelBase> Children => LazyLoadCompleted
? LazyLoadChildren
: _childrenList;
/// <summary>
/// How are the children held when in lazy loading mode.
/// </summary>
/// <remarks>
/// Override this as required in descendent classes - e.g. if Children is formed from a union
/// of multiple internal child item collections (of different types) which are populated in LazyLoadFetchChildren()
/// </remarks>
protected virtual IEnumerable<perTreeViewItemViewModelBase> LazyLoadChildren => _childrenList;
private bool _isSelected;
public bool IsSelected
{
get => _isSelected;
set
{
// if unselecting we don't care about anything else other than simply updating the property
if (!value)
{
Set(nameof(IsSelected), ref _isSelected, false);
return;
}
// Build a priority queue of operations
//
// All operations relating to tree item expansion are added with priority = DispatcherPriority.ContextIdle, so that they are
// sorted before any operations relating to selection (which have priority = DispatcherPriority.ApplicationIdle).
// This ensures that the visual container for all items are created before any selection operation is carried out.
//
// First expand all ancestors of the selected item - those closest to the root first
//
// Expanding a node will scroll as many of its children as possible into view - see perTreeViewItemHelper, but these scrolling
// operations will be added to the queue after all of the parent expansions.
var ancestorsToExpand = new Stack<perTreeViewItemViewModelBase>();
var parent = Parent;
while (parent != null)
{
if (!parent.IsExpanded)
{
ancestorsToExpand.Push(parent);
}
parent = parent.Parent;
}
while (ancestorsToExpand.Any())
{
var parentToExpand = ancestorsToExpand.Pop();
perDispatcherHelper.AddToQueue(() => parentToExpand.IsExpanded = true, DispatcherPriority.ContextIdle);
}
// Set the item's selected state - use DispatcherPriority.ApplicationIdle so this operation is executed after all
// expansion operations, no matter when they were added to the queue.
//
// Selecting a node will also scroll it into view - see perTreeViewItemHelper
perDispatcherHelper.AddToQueue(() => Set(nameof(IsSelected), ref _isSelected, true), DispatcherPriority.ApplicationIdle);
// note that by rule, a TreeView can only have one selected item, but this is handled automatically by
// the control - we aren't required to manually unselect the previously selected item.
// execute all of the queued operations in descending DispatcherPriority order (expansion before selection)
var unused = perDispatcherHelper.ProcessQueueAsync();
}
}
public override string ToString()
{
return Caption;
}
/// <summary>
/// What's the total number of child nodes beneath this one
/// </summary>
public int ChildCount => Children.Count() + Children.Sum(c => c.ChildCount);
}
IsExpanded 和 IsSelected 然后以全局樣式綁定到 TreeViewItem 的屬性。
<Style
x:Key="perTreeViewItemContainerStyle"
TargetType="{x:Type TreeViewItem}">
<Setter Property="IsEnabled" Value="{Binding IsEnabled}" />
<!-- Link the properties of perTreeViewItemViewModelBase to the corresponding ones on the TreeViewItem -->
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
// ....
</Style>
<Style TargetType="{x:Type TreeView}">
<Setter Property="ItemContainerStyle" Value="{StaticResource perTreeViewItemContainerStyle}" />
</Style>
然后,您可以為要在 TreeView 中顯示的每個 model 項目類型制作包裝器 class。 例如
public class PersonTreeViewWrapper: perTreeViewItemViewModelBase
{
public PersonTreeViewWrapper(Person model)
{
Model = model;
}
public Person Model {get;}
}
我不會嘗試將此包裝隱藏在控件內,而是在 ViewModel 中發布 PersonTreeViewWrapper 項的嵌套集合,以綁定為 TreeView 的 ItemsSource。
原因之一是如果您希望在同一個 TreeView 控件中顯示多個項目類型,您必須能夠指定要用於每個項目類型的 HierarchicalDataTemplate。
<TreeView
Grid.Column="0"
ItemsSource="{Binding RootItemVms}">
<TreeView.Resources>
<HierarchicalDataTemplate
DataType="{x:Type vm.PersonTreeViewWrapper}"
ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal">
<CheckBox
VerticalAlignment="Center"
Focusable="False"
IsChecked="{Binding IsChecked, Mode=TwoWay}" />
<TextBlock
Margin="4,0,8,0"
VerticalAlignment="Center"
Text="{Binding Model.DisplayName}" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
您還可以使用行為 class 將可綁定的 SelectedItem 屬性添加到 TreeView 控件。
public class perTreeViewHelper : Behavior<TreeView>
{
public object BoundSelectedItem
{
get => GetValue(BoundSelectedItemProperty);
set => SetValue(BoundSelectedItemProperty, value);
}
public static readonly DependencyProperty BoundSelectedItemProperty =
DependencyProperty.Register("BoundSelectedItem",
typeof(object),
typeof(perTreeViewHelper),
new FrameworkPropertyMetadata(null,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
OnBoundSelectedItemChanged));
private static void OnBoundSelectedItemChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
if (args.NewValue is perTreeViewItemViewModelBase item)
{
item.IsSelected = true;
}
}
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.SelectedItemChanged += OnTreeViewSelectedItemChanged;
}
protected override void OnDetaching()
{
AssociatedObject.SelectedItemChanged -= OnTreeViewSelectedItemChanged;
base.OnDetaching();
}
private void OnTreeViewSelectedItemChanged(object obj, RoutedPropertyChangedEventArgs<object> args)
{
BoundSelectedItem = args.NewValue;
}
}
您可以在我的博客文章中獲得有關這些類及其用法的更多詳細信息。
在我看來,你選擇了錯誤的方向來解決你的問題。 TreeView 這個名字有點誤導。 一棵樹只能有一個頂點,不能有環。 也就是說,樹中的所有節點(Item)都是唯一的。
查看 Windows Explorer 中 TreeView 的典型用法:多個入口點和相同的文件或文件夾可能位於不同的分支上。 因此,僅將 SelectedItem 設置為 select 項目是不夠的。 您需要提供該項目的完整路徑。
為了解決您的問題,我建議您不要創建自己的 UserControl,而是為 TreeView 創建一個附加屬性(比如說 SelectItem),將路徑傳遞到所選項目。
您可以通過不同的方式指定路徑:頂部的項目列表、帶有路徑的字符串以及其他變體。 在Attached Property邏輯中,當值發生變化時,路徑會隨着所需節點的選擇和展開,通過TreeView可視化動態樹進行解析和遍歷。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.