Collapse all expanders and expand one of them by default

I have several extenders, and I was looking for a way to collapse all other extensions when one of them expands. And I found this solution here

XAML:

<StackPanel Name="StackPanel1"> <StackPanel.Resources> <local:ExpanderToBooleanConverter x:Key="ExpanderToBooleanConverter" /> </StackPanel.Resources> <Expander Header="Expander 1" IsExpanded="{Binding SelectedExpander, Mode=TwoWay, Converter={StaticResource ExpanderToBooleanConverter}, ConverterParameter=1}"> <TextBlock>Expander 1</TextBlock> </Expander> <Expander Header="Expander 2" IsExpanded="{Binding SelectedExpander, Mode=TwoWay, Converter={StaticResource ExpanderToBooleanConverter}, ConverterParameter=2}"> <TextBlock>Expander 2</TextBlock> </Expander> <Expander Header="Expander 3" IsExpanded="{Binding SelectedExpander, Mode=TwoWay, Converter={StaticResource ExpanderToBooleanConverter}, ConverterParameter=3}"> <TextBlock>Expander 3</TextBlock> </Expander> <Expander Header="Expander 4" IsExpanded="{Binding SelectedExpander, Mode=TwoWay, Converter={StaticResource ExpanderToBooleanConverter}, ConverterParameter=4}"> <TextBlock>Expander 4</TextBlock> </Expander> </StackPanel> 

Converter:

 public class ExpanderToBooleanConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { return (value == parameter); // I tried thoses too : return value != null && (value.ToString() == parameter.ToString()); return value != null && (value.ToString().Equals(parameter.ToString())); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { return System.Convert.ToBoolean(value) ? parameter : null; } } 

ViewModel:

 public class ExpanderListViewModel : INotifyPropertyChanged { private Object _selectedExpander; public Object SelectedExpander { get { return _selectedExpander; } set { if (_selectedExpander == value) { return; } _selectedExpander = value; OnPropertyChanged("SelectedExpander"); } } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string propertyName) { PropertyChangedEventHandler handler = PropertyChanged; if (handler != null) { handler(this, new PropertyChangedEventArgs(propertyName)); } } } 

Initialization

 var viewModel = new ExpanderListViewModel(); StackPanel1.DataContext = viewModel; viewModel.SelectedExpander = 1; // I tried this also viewModel.SelectedExpander = "1"; 

It works fine, but now I want to expand one of the expanders when the application starts!

I already tried to put the values ​​(1, 2 or 3) in the SelectedExpander property, but none of the expanders will be expanded by default!

How to add this feature to my expanders?

+6
source share
6 answers

Think about what happens if you select UpdateSource on Expander 2 when Expander 1 is selected:

  • ConvertBack is called for Expander 2 with the current value IsExpanded ( false ) and returns null .
  • SelectedExpander updated to null .
  • Convert is called for all other expanders because the SelectedExpander changed, as a result of which all other IsExpanded values ​​will be set to false .

Of course, this is the wrong behavior. Thus, the decision depends on the fact that the source is never updated, unless the user actually switches the expander.

So, I suspect the problem is that initializing the controls somehow causes the source code to be updated. Even if Expander 1 was correctly initialized as extended, it would reset when the bindings were updated on any of the other extenders.

To make ConvertBack correct, it must know about other expanders: it should return only null if all of them are reset. However, I do not see a clean way to handle this from the converter. Perhaps the best solution would be to use one-way binding (no ConvertBack ) and handle Expanded and Collapsed events or similar (where _expanders is a list of all expander controls):

 private void OnExpanderIsExpandedChanged(object sender, RoutedEventArgs e) { var selectedExpander = _expanders.FirstOrDefault(e => e.IsExpanded); if (selectedExpander == null) { viewmodel.SelectedExpander = null; } else { viewmodel.SelectedExpander = selectedExpander.Tag; } } 

In this case, I use Tag for the identifier used in the viewmodel.

EDIT:

To solve the problem of more than "MVVM", you can have a collection of viewmodels for each expander with an individual property to bind IsExpanded to:

 public class ExpanderViewModel { public bool IsSelected { get; set; } // todo INotifyPropertyChanged etc. } 

Save the collection to ExpanderListViewModel and add PropertyChanged handlers for each of them during initialization:

 // in ExpanderListViewModel foreach (var expanderViewModel in Expanders) { expanderViewModel.PropertyChanged += Expander_PropertyChanged; } ... private void Expander_PropertyChanged(object sender, PropertyChangedEventArgs e) { var thisExpander = (ExpanderViewModel)sender; if (e.PropertyName == "IsSelected") { if (thisExpander.IsSelected) { foreach (var otherExpander in Expanders.Except(new[] {thisExpander})) { otherExpander.IsSelected = false; } } } } 

Then bind each expander to another item in the Expanders collection:

 <Expander Header="Expander 1" IsExpanded="{Binding Expanders[0].IsSelected}"> <TextBlock>Expander 1</TextBlock> </Expander> <Expander Header="Expander 2" IsExpanded="{Binding Expanders[1].IsSelected}"> <TextBlock>Expander 2</TextBlock> </Expander> 

(You can also examine the definition of ItemsControl to dynamically create extenders from a collection.)

In this case, the SelectedExpander property is no longer needed, but it can be implemented as follows:

 private ExpanderViewModel _selectedExpander; public ExpanderViewModel SelectedExpander { get { return _selectedExpander; } set { if (_selectedExpander == value) { return; } // deselect old expander if (_selectedExpander != null) { _selectedExpander.IsSelected = false; } _selectedExpander = value; // select new expander if (_selectedExpander != null) { _selectedExpander.IsSelected = true; } OnPropertyChanged("SelectedExpander"); } } 

And update the above PropertyChanged handler as:

 if (thisExpander.IsSelected) { ... SelectedExpander = thisExpander; } else { SelectedExpander = null; } 

So, now these two lines will be equivalent ways to initialize the first expander:

 viewModel.SelectedExpander = viewModel.Expanders[0]; viewModel.Expanders[0].IsSelected = true; 
+6
source

Change the Convert method (given here ) as follows

  if (value == null) return false; return (value.ToString() == parameter.ToString()); 

The previous content does not work due to comparing objects with the == operator.

0
source

I created a WPF project only with your code, having a StackPanel as the content of MainWindow and calling the initialization code after calling InitializeComponent() in MainWindow() and works like a charm by simply removing

 return (value == parameter); 

from ExpanderToBooleanConverter.Convert . In fact, @Boopesh answer works too. Even if you do

 return ((string)value == (string)parameter); 

it works, but in this case only string values ​​are supported for SelectedExpander .

I suggest you try these other returns in your Convert again, and if that doesn't work, your problem might be in your initialization code. You might be installing SelectedExpander before the components are properly initialized.

0
source

I wrote an example code that demonstrates how to achieve what you want.

 <ItemsControl ItemsSource="{Binding Path=Items}"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <StackPanel /> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemTemplate> <DataTemplate> <RadioButton GroupName="group"> <RadioButton.Template> <ControlTemplate> <Expander Header="{Binding Path=Header}" Content="{Binding Path=Content}" IsExpanded="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=IsChecked}" /> </ControlTemplate> </RadioButton.Template> </RadioButton> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> 

The model looks like this:

 public class Model { public string Header { get; set; } public string Content { get; set; } } 

And the ViewModel exposes the model to the view:

 public IList<Model> Items { get { IList<Model> items = new List<Model>(); items.Add(new Model() { Header = "Header 1", Content = "Header 1 content" }); items.Add(new Model() { Header = "Header 2", Content = "Header 2 content" }); items.Add(new Model() { Header = "Header 3", Content = "Header 3 content" }); return items; } } 

If you don't create a view model (maybe it's static), you can use the x: Array markup extension.

you can find an example here

0
source

You need to set the property after the Loaded view.

Xaml

 <Window x:Class="UniformWindow.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local ="clr-namespace:UniformWindow" Title="MainWindow" Loaded="Window_Loaded"> <!- your XAMLSnipped goes here-> </Window> 

Codebehind

 public partial class MainWindow : Window { ExpanderListViewModel vm = new ExpanderListViewModel(); public MainWindow() { InitializeComponent(); StackPanel1.DataContext = vm; } private void Window_Loaded(object sender, RoutedEventArgs e) { vm.SelectedExpander = "2"; } } 

IValueConverter

 public class ExpanderToBooleanConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { // to prevent NullRef if (value == null || parameter == null) return false; var sValue = value.ToString(); var sparam = parameter.ToString(); return (sValue == sparam); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { if (System.Convert.ToBoolean(value)) return parameter; return null; } } 
0
source

I did it like this:

 <StackPanel Name="StackPanel1"> <Expander Header="Expander 1" Expanded="Expander_Expanded"> <TextBlock>Expander 1</TextBlock> </Expander> <Expander Header="Expander 2" Expanded="Expander_Expanded"> <TextBlock>Expander 2</TextBlock> </Expander> <Expander Header="Expander 3" Expanded="Expander_Expanded" > <TextBlock>Expander 3</TextBlock> </Expander> <Expander Header="Expander 4" Expanded="Expander_Expanded" > <TextBlock>Expander 4</TextBlock> </Expander> </StackPanel> private void Expander_Expanded(object sender, RoutedEventArgs e) { foreach (Expander exp in StackPanel1.Children) { if (exp != sender) { exp.IsExpanded = false; } } } 
0
source

All Articles