簡體   English   中英

是否有任何從 IEnumerable 獲得的包裝 object class? (TreeView 中的 ItemsSource)

[英]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可視化動態樹進行解析和遍歷。

如何從 IEnumerable 獲取屬性值<object><div id="text_translate"><p>我有一個返回 IEnumerable 的方法</p><pre>public static IEnumerable&lt;object&gt; GetProps&lt;T&gt;(T obj) { var result = obj.GetType().GetProperties().Select(x =&gt; new { property = x.Name, value = x.GetValue(obj) }).Where(x =&gt; x.value == null).ToList(); return result; }</pre><p> 上面的代碼將返回結果為[{"property":"YearOfBirth","value":null}]</p><p> 我現在嘗試從返回的結果中獲取屬性值<strong>YearOfBirth</strong> 。 有人可以建議/幫助嗎?</p></div></object>

[英]How to get property value from IEnumerable<object>

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM