WPF ListView bound to collection - find parent expander

456 views Asked by At

I've writing an app that displays a list of drawings, as a test I've written a small app that displays how I envisage it working.

The app works by:

  • Creating a BrowserItem class, this has a name property, active property and three levels of grouping.

  • Has a WPF ListView which is bound to an ObservableCollection, and uses an ICollectionView to display the BrowserItems in the list view nested based on the three levels of grouping

  • When the app runs, it creates several BrowserItems, sets their names and grouping etc and displays the items in the ListView

This all works well.

As a test the app has a slider which depending on the slider number simply set the Active property of a BrowserItem to true - this causes it to be highlighted in green in the list view as Active is a bound property to the font background.

What I would like to happen is when its highlighted in green, the routine will automatically expand all Expanders so the user can see the highlighted item without having to hunt for it.

Is there any way I can get the Browser items parent expander?

The code can be downloaded here: https://drive.google.com/file/d/1MW-v2gqFtubhnZdKUYGglQLN3S5xnVB4/view?usp=sharing

BrowserItem Class:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ListBoxTest3
{

    public partial class BrowserItem : ViewModelBase
    {
        /// <summary>
        /// BrowserItem represents a node on the browser treeview structure and relates to either a browser branch or a view / sheet
        /// </summary>

        //Group one nesting level
        private string _l1group;
        public string L1group
        {
            get
            {
                return _l1group;
            }
             set
            {
                _l1group = value;
                OnPropertyChanged(nameof(L1group));
            }
        }

        //Group two nesting level
        private string _l2group;
        public string L2group
        {
            get
            {
                return _l2group;
            }
            set
            {
                _l2group = value;
                OnPropertyChanged(nameof(L2group));
            }
        }

        //Group three nesting level
        private string _l3group;
        public string L3group
        {
            get
            {
                return _l3group;
            }
            set
            {
                _l3group = value;
                OnPropertyChanged(nameof(L3group));
            }
        }


        //Name
        private string _name;

        public string Name
        {
            get => _name;
            set
            {
                if (_name != value)
                {
                    _name = value;
                    OnPropertyChanged(nameof(Name));
                }
            }
        }


        //Active or _active = is bound to the active view highlight
        private bool _active;

        public bool Active
        {
            get => _active;
            set
            {
                if (value != _active)
                {
                    _active = value;
                    OnPropertyChanged(nameof(Active));
                }
            }
        }
    }
}

MainWindow.Xaml:

<Window x:Class="ListBoxTest3.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ListBoxTest3"
        mc:Ignorable="d"
        Title="MainWindow"
        Height="450"
        Width="400">

    <Grid>
        <StackPanel Orientation="Vertical">
            <StackPanel Orientation="Vertical">
                <StackPanel Orientation="Horizontal"
                            Margin="10">
                    <TextBlock Text="Filter: " />
                    <TextBox x:Name="FilterBoxText"
                             Width="330"
                             Text="{Binding BrowserItemFilter, UpdateSourceTrigger=PropertyChanged}" />
                </StackPanel>
                <StackPanel Orientation="Vertical">
                    <StackPanel Orientation="Horizontal"
                                Margin="10">
                        <TextBlock Text="Change Active to number: " />
                        <Slider x:Name="changeactive"
                                Width="210"
                                SmallChange="1"
                                ValueChanged="Slider_ValueChanged"
                                IsSnapToTickEnabled="True"
                                TickFrequency="1"
                                TickPlacement="Both">

                        </Slider>
                        <TextBlock x:Name="displayslider"
                                   Text="0" />
                    </StackPanel>
                </StackPanel>
            </StackPanel>
            <ListView Name="TreeViewSheets"
                      Margin="10,10,10,10"
                      Height="314"
                      ItemsSource="{Binding BrowserItemCollectionView}">

                <ListView.ItemTemplate>
                    <DataTemplate>

                        <WrapPanel>
                            <TextBlock Text="{Binding Name}">
                                <TextBlock.Style>
                                    <Style TargetType="{x:Type TextBlock}">
                                        <Style.Triggers>
                                            <DataTrigger Binding="{Binding Active}"
                                                         Value="true">
                                                <Setter Property="Background"
                                                        Value="YellowGreen" />
                                            </DataTrigger>
                                        </Style.Triggers>
                                    </Style>
                                </TextBlock.Style>
                            </TextBlock>
                        </WrapPanel>

                    </DataTemplate>
                </ListView.ItemTemplate>

                <ListView.GroupStyle>
                    <GroupStyle>
                        <GroupStyle.ContainerStyle>
                            <Style TargetType="{x:Type GroupItem}">
                                <Setter Property="Template">
                                    <Setter.Value>
                                        <ControlTemplate>

                                            <Expander>

                                                <Expander.Header>
                                                    <TextBlock Text="{Binding Name}"
                                                               FontWeight="Bold"
                                                               VerticalAlignment="Bottom" />
                                                </Expander.Header>
                                                <ItemsPresenter />
                                            </Expander>

                                        </ControlTemplate>
                                    </Setter.Value>
                                </Setter>
                            </Style>
                        </GroupStyle.ContainerStyle>
                    </GroupStyle>
                </ListView.GroupStyle>

            </ListView>
        </StackPanel>
    </Grid>
</Window>

Code behind MainWindow.xaml:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;

namespace ListBoxTest3
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>

    public partial class MainWindow : Window
    {
        MainWindowViewModel store
        {
            get;
            set;
        }

        public MainWindow()
        {
            InitializeComponent();

            store = new MainWindowViewModel();

            store.Browser.Add(new BrowserItem() { Name = "Level 01 Plan", L1group = "Architectural", Active = true, L2group = "Floor Plans", L3group = "1:100" }); ;
            store.Browser.Add(new BrowserItem() { Name = "Level 01 Plan", L1group = "Structural", Active = false, L2group = "Floor Plans", L3group = "1:20" });
            store.Browser.Add(new BrowserItem() { Name = "Level 02 Plan", L1group = "Architectural", Active = false, L2group = "Floor Plans", L3group = "1:100" });
            store.Browser.Add(new BrowserItem() { Name = "Level 02 Plan", L1group = "Structural", Active = false, L2group = "Floor Plans", L3group = "1:100" });
            store.Browser.Add(new BrowserItem() { Name = "Elevation East", L1group = "Architectural", Active = false, L2group = "Elevations", L3group = "1:100" });
            store.Browser.Add(new BrowserItem() { Name = "Elevation West", L1group = "Architectural", Active = false, L2group = "Elevations", L3group = "1:20" });
            store.Browser.Add(new BrowserItem() { Name = "Elevation North", L1group = "Architectural", Active = false, L2group = "Elevations", L3group = "1:100" });
            store.Browser.Add(new BrowserItem() { Name = "Section A-A", L1group = "Architectural", Active = false, L2group = "Sections", L3group = "1:100" });
            store.Browser.Add(new BrowserItem() { Name = "Section B-B", L1group = "Architectural", Active = false, L2group = "Sections", L3group = "1:100" });
            store.Browser.Add(new BrowserItem() { Name = "Section C-C", L1group = "Structural", Active = false, L2group = "Sections", L3group = "1:100" });
            store.Browser.Add(new BrowserItem() { Name = "Main Section", L1group = "Structural", Active = false, L2group = "Main Sections", L3group = "1:100" });
            store.Browser.Add(new BrowserItem() { Name = "Level 01 RCP", L1group = "Architectural", Active = false, L2group = "Ceiling Plans", L3group = "1:100" });

            TreeViewSheets.DataContext = store;

            FilterBoxText.DataContext = store;

            //Set slider max value
            changeactive.Maximum = store.Browser.Count() - 1;
        }

        private void Slider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            //When slider is moved, set the item in the list as active
            for (int f=0; f < store.Browser.Count; f++)
            {
                if (f == changeactive.Value)
                {
                    store.Browser[f].Active = true;
                    displayslider.Text = f.ToString();
                    TreeViewSheets.ScrollIntoView(store.Browser[f]);
                }
                else
                {
                    store.Browser[f].Active = false;
                }
            }
        }
    }
}

MainWindowViewModel class, this contains the ObservableCollection that contains the browser items. It also has a constructor for MainWindowViewModel that groups using a CollectionViewSource - this dynamically creates the group expanders for the Listview based on the L1Group, L2Group and L3Group fields of the BrowserItem class:

    class MainWindowViewModel : ViewModelBase
{
    public ICollectionView BrowserItemCollectionView { get; }

    private ObservableCollection<BrowserItem> _browser = new ObservableCollection<BrowserItem>();

    public ObservableCollection<BrowserItem> Browser
    {
        get => _browser;
        set
        {
            if (value != _browser)
            {
                _browser = value;
                OnPropertyChanged(nameof(Browser));
            }
        }
    }

    private string _browserItemFilter = string.Empty;
    public string BrowserItemFilter
    {
        get
        {
            return _browserItemFilter;
        }
        set
        {
            _browserItemFilter = value;
            OnPropertyChanged(nameof(BrowserItemFilter));
            BrowserItemCollectionView.Refresh();
        }
    }

    public MainWindowViewModel()
    //Constructor
    {
        BrowserItemCollectionView = CollectionViewSource.GetDefaultView(_browser);

        //Set up filter
        BrowserItemCollectionView.Filter = FilterBrowserItems;

        //Set up grouping
        BrowserItemCollectionView.GroupDescriptions.Add(new PropertyGroupDescription(nameof(BrowserItem.L1group)));
        BrowserItemCollectionView.GroupDescriptions.Add(new PropertyGroupDescription(nameof(BrowserItem.L2group)));
        BrowserItemCollectionView.GroupDescriptions.Add(new PropertyGroupDescription(nameof(BrowserItem.L3group)));
    }

    private bool FilterBrowserItems(object obj)
    {
        if (obj is BrowserItem check)
        {
            return check.Name.Contains(BrowserItemFilter);
        }

        return false;
    }
}
1

There are 1 answers

30
BionicCode On

You can name the Expander and then look it up in the template of the group item by its name or use the VisualTreeHelper to traverse the visual tree to find the Expander:

private void Slider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
  //When slider is moved, set the item in the list as active
  for (int index = 0; index < store.Browser.Count; index++)
  {
    BrowserItem browserItem  = store.Browser[index];

    if (index == e.NewValue)
    { 
      browserItem .Active = true;
      displayslider.Text = index.ToString();
      TreeViewSheets.ScrollIntoView(browserItem);

      if (TryGetExpanderOfItem(browserItem, out Expander expander))
      {
        expander.Active = true; 
      }
    }
    else
    {
      browserItem.Active = false;
    }
  }
}

private bool TryGetExpanderOfItem(object browserItem, out Expander expander)
{
  expander = null;

  CollectionViewGroup groupItem = this.TreeViewSheets.Items.Groups
    .OfType<CollectionViewGroup>()
    .FirstOrDefault(group => group.Items.Contains(browserItem));

  if (groupItem == null)
  {
    return false;
  }

  var groupContainer = this.TreeViewSheets.ItemContainerGenerator.ContainerFromItem(groupItem) as Control;
  expander = groupContainer?.Template.FindName("GroupExpander", groupContainer) as Expander;

  return expander != null;
}

Find and expand all Expander elements in group

The following version returns all Expander elements in the visual tree starting from the selected group item:

private void Slider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
  //When slider is moved, set the item in the list as active
  for (int f = 0; f < store.Browser.Count; f++)
  {
    BrowserItem browserItem = store.Browser[f];

    if (f == e.NewValue)
    {
      browserItem.Active = true;
      displayslider.Text = f.ToString();
      TreeViewSheets.ScrollIntoView(browserItem);

      foreach (CollectionViewGroup collectionViewGroup in this.TreeViewSheets.Items.Groups
        .OfType<CollectionViewGroup>())
      {
        if (TryCollectParentGroups(collectionViewGroup, browserItem, out IList<CollectionViewGroup> groupPath))
        {
          foreach (Expander expander in EnumerateExpandersOfItem(groupPath))
          {
            expander.IsExpanded = true;
          }

          break;
        }
      }
    }
    else
    {
      browserItem.Active = false;
    }
  }
}

private bool TryCollectParentGroups(CollectionViewGroup parentGroup, object referenceItem, out IList<CollectionViewGroup> groupPath)
{
  groupPath = new List<CollectionViewGroup>();
  foreach (object parentGroupItem in parentGroup.Items)
  {
    if (object.ReferenceEquals(parentGroupItem, referenceItem))
    {
      groupPath = new List<CollectionViewGroup>() {parentGroup};
      return true;
    }

    if (parentGroupItem is CollectionViewGroup childGroup)
    {
      if (TryCollectParentGroups(childGroup, referenceItem, out IList<CollectionViewGroup> childGroupPath))
      {
        groupPath = childGroupPath;
        groupPath.Insert(0, parentGroup);
        return true;
      }
    }
  }

  return false;
}

private IEnumerable<Expander> EnumerateExpandersOfItem(IList<CollectionViewGroup> groupPath)
{
  if (!groupPath.Any())
  {
    return Enumerable.Empty<Expander>();
  }

  CollectionViewGroup rootGroup = groupPath.First();
  groupPath.RemoveAt(0);
  var groupItemContainer = this.TreeViewSheets.ItemContainerGenerator.ContainerFromItem(rootGroup) as GroupItem;
  return EnumerateGroupPath(groupItemContainer, groupPath);
}

private IEnumerable<Expander> EnumerateGroupPath(
  GroupItem groupItemContainer,
  IList<CollectionViewGroup> groupPath)
{
  groupItemContainer.ApplyTemplate();
  foreach (Expander expander in groupItemContainer.EnumerateVisualChildElements<Expander>())
  {
    yield return expander;

    if (!groupPath.Any())
    {
      yield break;
    }

    (expander.Content as ItemsPresenter).ApplyTemplate();
    if ((expander.Content as ItemsPresenter).TryFindVisualChildElement(out Panel childGroupsHost))
    {
      CollectionViewGroup currentGroupNode = groupPath.First();
      IEnumerable<GroupItem> childGroups = childGroupsHost.Children.OfType<GroupItem>();
      GroupItem childGroupInPath = childGroups.FirstOrDefault(group => ReferenceEquals(group.Content, currentGroupNode));
      if (childGroupInPath == null)
      {
        continue;
      }
      groupPath.RemoveAt(0);
      foreach (Expander childExpander in EnumerateGroupPath(childGroupInPath, groupPath))
      {
        yield return childExpander;
      }
    }
  }
}