簡體   English   中英

DataTemplate 中 TreeViewItem 的 IsExpanded 屬性

[英]IsExpanded property of TreeViewItem within DataTemplate

我只想實現的是從代碼隱藏中更改所有 TreeViewItems 的展開/折疊狀態。 我為兩個按鈕創建了兩個事件處理程序:

private void Button_Click(object sender, RoutedEventArgs e)
{
        for(int i=0;i<trv.Items.Count;i++)
        {
                TreeViewItem item = (TreeViewItem)(trv.ItemContainerGenerator.ContainerFromIndex(i));
                item.IsExpanded = false;
        }
}

private void Button_Click1(object sender, RoutedEventArgs e)
{
        for (int i = 0; i < trv.Items.Count; i++)
        {
                TreeViewItem item = (TreeViewItem)(trv.ItemContainerGenerator.ContainerFromIndex(i));
                item.IsExpanded = true;
            }
        }

和我的 XAML 的 TreeView 部分:

<TreeView Name="trv" ItemsSource="{Binding modelItems}">
        <TreeView.ItemTemplate>
                <DataTemplate>
                        <TreeViewItem  ItemsSource="{Binding modelSubItems}">
                                <TreeViewItem.Header>
                                        <Grid Width="100">
                                                <Grid.ColumnDefinitions>
                                                        <ColumnDefinition Width="50"></ColumnDefinition>
                                                        <ColumnDefinition Width="50"></ColumnDefinition>
                                                </Grid.ColumnDefinitions>
                                        <TextBox Text="{Binding itemId}"/>
                                        <TextBox Text="{Binding itemName}"/>
                                </TreeViewItem.Header>
                                <TreeViewItem.ItemTemplate>
                                        <DataTemplate>
                                                <Grid Margin="-20,0,0,0">
                                                        <Grid.ColumnDefinitions>
                                                                <ColumnDefinition Width="50"></ColumnDefinition>
                                                                <ColumnDefinition Width="50"></ColumnDefinition>
                                                        </Grid.ColumnDefinitions>
                                                        <TextBox Text="{Binding subItemId}"/>
                                                        <TextBox Text="{Binding subItemName}"/>
                                                </Grid>
                                        </DataTemplate>
                                <TreeViewItem.ItemTemplate>
                        </TreeViewItem>
                </DataTemplate>
        </TreeView.ItemTemplate>
</TreeView>

這不起作用,TreeView 項目不會對從代碼隱藏更改的 IsExpanded 做出反應。

許多消息來源說問題出在 DataTemplate 中。 所以,我改變了我的 XAML 添加 TreeView.ItemContainerStyle:

<TreeView Name="trv" ItemsSource="{Binding modelItems}">
        <TreeView.ItemContainerStyle>
                <Style TargetType="{x:Type TreeViewItem}">
                        <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
                </Style>
        </TreeView.ItemContainerStyle>
        <TreeView.ItemTemplate>
                <DataTemplate>
                        <TreeViewItem  ItemsSource="{Binding modelSubItems}">
                                <TreeViewItem.Header>
                                        <Grid Width="100">
                                                <Grid.ColumnDefinitions>
                                                        <ColumnDefinition Width="50"></ColumnDefinition>
                                                        <ColumnDefinition Width="50"></ColumnDefinition>
                                                </Grid.ColumnDefinitions>
                                        <TextBox Text="{Binding itemId}"/>
                                        <TextBox Text="{Binding itemName}"/>
                                </TreeViewItem.Header>
                                <TreeViewItem.ItemTemplate>
                                        <DataTemplate>
                                                <Grid Margin="-20,0,0,0">
                                                        <Grid.ColumnDefinitions>
                                                                <ColumnDefinition Width="50"></ColumnDefinition>
                                                                <ColumnDefinition Width="50"></ColumnDefinition>
                                                        </Grid.ColumnDefinitions>
                                                        <TextBox Text="{Binding subItemId}"/>
                                                        <TextBox Text="{Binding subItemName}"/>
                                                </Grid>
                                        </DataTemplate>
                                <TreeViewItem.ItemTemplate>
                        </TreeViewItem>
                </DataTemplate>
        </TreeView.ItemTemplate>
</TreeView>

現在。 我應該在哪里放置 IsExpanded 定義? 在模型視圖中,在模型中? 我都試過,沒有運氣。 當放置在 ModelView 中時,我得到了輸出:

System.Windows.Data Error: 40 : BindingExpression path error: 'IsExpanded' property not found on 'object' ''modelItems' (HashCode=43304686)'. BindingExpression:Path=IsExpanded; DataItem='modelItems' (HashCode=43304686); target element is 'TreeViewItem' (Name=''); target property is 'IsExpanded' (type 'Boolean')

當放置在模型中時,沒有綁定錯誤,但仍然不起作用。 當然,在(模型和模型視圖)中,我都實現了 INotifyPropertyChanged,它通常有效:

public class ModelItem : INotifyPropertyChanged
{
        (...)
        public event PropertyChangedEventHandler PropertyChanged;
        (...)
        protected void OnPropertyChanged([CallerMemberName] string name = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }
}

我在 MVVM 上下文中處理 WPF TreeView 控件的方式是通過使用公共基類來保存每個節點的數據。

/// <summary>
/// A base class for items that can be displayed in a TreeView or other hierarchical display
/// </summary>
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 of 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);
}

然后以全局樣式定義數據和您需要的 UI 控件之間的 IsExpanded 鏈接。

<Style
    x:Key="perTreeViewItemContainerStyle"
    TargetType="{x:Type TreeViewItem}">

    <!--  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 的數據構建為 perTreeViewItemViewModelBase 后代項的嵌套集合,並根據需要設置每個數據項的 IsExpanded 屬性。 請注意,MVVM 的關鍵原則是 UI 和數據的分離,因此除了樣式之外,不要在任何地方提及 TreeViewItem。

此 perTreeViewItemViewModelBase 類的更多詳細信息及其在我的博客文章中的用法。

暫無
暫無

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

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