16

I have quite a tricky problem:

I am using a ListView control with the ItemsSource set to a CollectionViewSource including a PropertyGroupDescription to group the ListView elements. The CollectionViewSource looks like this:

<CollectionViewSource x:Key="ListViewObjects">
   <CollectionViewSource.Source>
      <Binding Path="CurrentListViewData"/>
   </CollectionViewSource.Source>
   <CollectionViewSource.GroupDescriptions>
      <PropertyGroupDescription PropertyName="ObjectType" />
   </CollectionViewSource.GroupDescriptions>
</CollectionViewSource>

In the ListView I use customize the group headers like this:

<ListView.GroupStyle>
   <GroupStyle>
      <GroupStyle.ContainerStyle>
         <Style TargetType="{x:Type GroupItem}">
            <Setter Property="Margin" Value="5"/>
            <Setter Property="Template">
               <Setter.Value>
                  <ControlTemplate TargetType="{x:Type GroupItem}">
                     <Expander IsExpanded="True">
                        <Expander.Header>
                           <DockPanel>
                              <TextBlock Text="{Binding Path=Items[0].ObjectType />
                           </DockPanel>
                        </Expander.Header>
                        <Expander.Content>
                           <ItemsPresenter />
                        </Expander.Content>
                     </Expander>
                  </ControlTemplate>
               </Setter.Value>
            </Setter>
         </Style>
      </GroupStyle.ContainerStyle>
   </GroupStyle>
</ListView.GroupStyle>

As you can see the IsExpanded property of the Expander is set to true. This means that whenever the ListView is refreshed, all Expander controls are expanded.

I do however want to save the last state of every Expander. I haven't been able to figure out a way to save a list of Expander states per ObjectType. I was experimenting with a bound HashTable and a Converter, but I failed at providing the ObjectType as a ConverterParameter, because it was always passed as a string. But that may not be the solution anyways.

Can somebody give me a hint or an idea for a solution, please? :)

Holger Adam
  • 765
  • 12
  • 16

3 Answers3

21

The accepted answer is wrong as explained in the comments. I wrote the following behavior which achieves the desired functionality:

public class PersistGroupExpandedStateBehavior : Behavior<Expander>
{
    #region Static Fields

    public static readonly DependencyProperty GroupNameProperty = DependencyProperty.Register(
        "GroupName", 
        typeof(object), 
        typeof(PersistGroupExpandedStateBehavior), 
        new PropertyMetadata(default(object)));

    private static readonly DependencyProperty ExpandedStateStoreProperty =
        DependencyProperty.RegisterAttached(
            "ExpandedStateStore", 
            typeof(IDictionary<object, bool>), 
            typeof(PersistGroupExpandedStateBehavior), 
            new PropertyMetadata(default(IDictionary<object, bool>)));

    #endregion

    #region Public Properties

    public object GroupName
    {
        get
        {
            return (object)this.GetValue(GroupNameProperty);
        }

        set
        {
            this.SetValue(GroupNameProperty, value);
        }
    }

    #endregion

    #region Methods

    protected override void OnAttached()
    {
        base.OnAttached();

        bool? expanded = this.GetExpandedState();

        if (expanded != null)
        {
            this.AssociatedObject.IsExpanded = expanded.Value;
        }

        this.AssociatedObject.Expanded += this.OnExpanded;
        this.AssociatedObject.Collapsed += this.OnCollapsed;
    }

    protected override void OnDetaching()
    {
        this.AssociatedObject.Expanded -= this.OnExpanded;
        this.AssociatedObject.Collapsed -= this.OnCollapsed;

        base.OnDetaching();
    }

    private ItemsControl FindItemsControl()
    {
        DependencyObject current = this.AssociatedObject;

        while (current != null && !(current is ItemsControl))
        {
            current = VisualTreeHelper.GetParent(current);
        }

        if (current == null)
        {
            return null;
        }

        return current as ItemsControl;
    }

    private bool? GetExpandedState()
    {
        var dict = this.GetExpandedStateStore();

        if (!dict.ContainsKey(this.GroupName))
        {
            return null;
        }

        return dict[this.GroupName];
    }

    private IDictionary<object, bool> GetExpandedStateStore()
    {
        ItemsControl itemsControl = this.FindItemsControl();

        if (itemsControl == null)
        {
            throw new Exception(
                "Behavior needs to be attached to an Expander that is contained inside an ItemsControl");
        }

        var dict = (IDictionary<object, bool>)itemsControl.GetValue(ExpandedStateStoreProperty);

        if (dict == null)
        {
            dict = new Dictionary<object, bool>();
            itemsControl.SetValue(ExpandedStateStoreProperty, dict);
        }

        return dict;
    }

    private void OnCollapsed(object sender, RoutedEventArgs e)
    {
        this.SetExpanded(false);
    }

    private void OnExpanded(object sender, RoutedEventArgs e)
    {
        this.SetExpanded(true);
    }

    private void SetExpanded(bool expanded)
    {
        var dict = this.GetExpandedStateStore();

        dict[this.GroupName] = expanded;
    }

    #endregion
}

It attaches a dictionary to the containing ItemsControl which saves the expanded state for every group item. This will be peristent, even if the items in the control changes.

Usage:

<Expander>
    <i:Interaction.Behaviors>
        <behaviors:PersistGroupExpandedStateBehavior GroupName="{Binding Name}" />
    </i:Interaction.Behaviors>
    ...
</Expander>
aKzenT
  • 7,775
  • 2
  • 36
  • 65
  • 2
    re: i:Interaction.Behavior -- see: http://stackoverflow.com/questions/18778490/i-interaction-behavior-option-is-not-coming-for-applying-beahviour and also http://stackoverflow.com/questions/3059821/the-tag-interaction-behaviors-does-not-exist-in-vs2010-blend-3 – Jesse Chisholm Jun 29 '14 at 13:45
  • 2
    This worked out well, if by chance the GroupName is null, it will throw an error. I modified my own behavior to be GroupName ?? string.Empty in the places it was being used. This was a very nice add though. Thank you! – TravisWhidden Oct 11 '17 at 23:25
9

You could create a new class with a Dictionary (say, ObjectType as a key to bool values), and give it an indexer:

    Dictionary<ObjectType, bool> expandStates = new Dictionary<ObjectType, bool>();

    public bool this[ObjectType key]
    {
        get
        {
            if (!expandStates.ContainsKey(key)) return false;
            return expandStates[key];
        }
        set
        {
            expandStates[key] = value;
        }
    }

Then, instantiate it in a ResourceDictionary somewhere and bind the IsExpanded to it like this:

<Expander IsExpanded="{Binding Source={StaticResource myExpMgr}, Path=[Items[0].ObjectType]}">

That might well do it: a nice way of getting WPF to call your code and pass a parameter just when you need it. (That WPF lets you put subexpressions in indexers in a binding path was news to me - good though isn't it!)

Kieren Johnstone
  • 41,277
  • 16
  • 94
  • 144
  • I should also add that rather than using NotifyPropertyChanged() you could bind to an object that implements INotifyCollectionChanged (http://msdn.microsoft.com/en-us/library/system.collections.specialized.inotifycollectionchanged.aspx), or use an ObservableCollection. Not sure that it wouldn't cause the same issues though! – Kieren Johnstone Jun 25 '10 at 17:57
  • Awesome sauce! LOVE this! Thanks! I'm going to use it for a TreeView for the same thing. The best part is you can store so much more than just an IsExpanded. I'm actually going to use it to store Viewmodel information regarding the auto-created nodes that are based on the grouping. – Mark A. Donohoe Mar 29 '11 at 01:08
  • 1
    Nice though it would be if this worked, unfortunately, it doesn't, as far as I can tell. At least, not in .NET 4.5. The sub-expression in the indexer is passed as a string to the indexer, and not evaluated. – Mark Dec 11 '12 at 11:12
  • 1
    Did anyone of the upvoters actually try this code? IT DOES NOT WORK. As @Mark already mentioned, this will pass everything inside [...] as a string to the indexer. Subexpressions in Indexers are not possible in WPF. See also this question: http://stackoverflow.com/questions/4511619/wpf-bind-to-indexer – aKzenT Apr 10 '13 at 08:45
  • Works for me in 2 separate projects. It could be a .NET 4.5 thing? – Kieren Johnstone Apr 10 '13 at 12:27
  • Do you use the actual syntax like presented here? Everything you enter after Path= gets passed as a string. This is in a .NET 4.0 project. – aKzenT Apr 10 '13 at 16:02
  • There is also no documentation from Microsoft that would indicate that this is possible. – aKzenT Apr 10 '13 at 16:03
  • Here is a simplified example of subexpressions in Indexers, which does not work: http://pastebin.com/WJSLbUgr Maybe you can give it a try. – aKzenT Apr 10 '13 at 16:33
1

The marked Answer Doesn't work in .Net 4.5.

A simple solution for this is to add a

    private bool _isExpanded = true;
    public bool IsExpanded
    {
        get { return _isExpanded; }
        set { _isExpanded = value; }
    }

property to your ViewModel (In this case Whatever objects CurrentListViewData holds)

and then do:

<Expander IsExpanded="{Binding Items[0].IsExpanded}">

on your template.

Simple and effective just like WPF should be

JKennedy
  • 18,150
  • 17
  • 114
  • 198
  • Do I understand correctly, that you want to save the "IsExpanded" property that belongs to the list itself to the first item in the list? What if the list is empty or the first item gets removed? – Woozar Oct 19 '16 at 10:02