简体   繁体   English

DataTemplate 中 TreeViewItem 的 IsExpanded 属性

[英]IsExpanded property of TreeViewItem within DataTemplate

What I simply want to achieve is to change expand/collapse state of all TreeViewItems from Code Behind.我只想实现的是从代码隐藏中更改所有 TreeViewItems 的展开/折叠状态。 I have created two event handlers for two buttons:我为两个按钮创建了两个事件处理程序:

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;
            }
        }

And my TreeView part of XAML:和我的 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>

This does not work, TreeView items do not react on IsExpanded changing from Code Behind.这不起作用,TreeView 项目不会对从代码隐藏更改的 IsExpanded 做出反应。

Many sources say that the problem is in DataTemplate.许多消息来源说问题出在 DataTemplate 中。 So, I have changed my XAML adding TreeView.ItemContainerStyle:所以,我改变了我的 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>

And now.现在。 Where should I place IsExpanded definition?我应该在哪里放置 IsExpanded 定义? In ModelView, in Model?在模型视图中,在模型中? I have tried both, no luck.我都试过,没有运气。 When placed in ModelView I am getting in output:当放置在 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')

When placed in Model, no Binding errors, but still doesn't work.当放置在模型中时,没有绑定错误,但仍然不起作用。 Of course in both (Model and ModelView), I have INotifyPropertyChanged implemented, which generally works:当然,在(模型和模型视图)中,我都实现了 INotifyPropertyChanged,它通常有效:

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

The way I handle WPF TreeView controls in a MVVM context is through the use of a common base class to hold the data for each node.我在 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);
}

The IsExpanded link between data and UI controls that you require is then defined in a global style.然后以全局样式定义数据和您需要的 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>

You can then build the data for the TreeView as a nested collection of perTreeViewItemViewModelBase descendent items, and set the IsExpanded property of each data item as required.然后,您可以将 TreeView 的数据构建为 perTreeViewItemViewModelBase 后代项的嵌套集合,并根据需要设置每个数据项的 IsExpanded 属性。 Note that the key tenet of MVVM is separation of UI and data, so not a single mention of TreeViewItem anywhere other than in the style.请注意,MVVM 的关键原则是 UI 和数据的分离,因此除了样式之外,不要在任何地方提及 TreeViewItem。

More details of this perTreeViewItemViewModelBase class and its usage on my blog post .此 perTreeViewItemViewModelBase 类的更多详细信息及其在我的博客文章中的用法。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM