I have multiple expanders, and I was looking for a way to collapse all others the expanders when one of them is expanded. And I found this solution here
<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>
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;
}
}
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));
}
}
}
var viewModel = new ExpanderListViewModel();
StackPanel1.DataContext = viewModel;
viewModel.SelectedExpander = 1;
// I tried this also
viewModel.SelectedExpander = "1";
It's working fine, but now I want to expand one of the expanders at the application startup !
I already tried to put the values (1, 2 or 3) in SelectedExpander property, but none of expanders get expanded by default !
How can I add this possibility to my expanders ?
Consider what would happen if you called UpdateSource on Expander 2 while Expander 1 is selected:
ConvertBack
is called for Expander 2 with its current IsExpanded
value (false
), and returns null
.SelectedExpander
is updated to null
.Convert
is called for all other expanders, because SelectedExpander
changed, causing all the other IsExpanded
values to be set to false
as well.This isn't the correct behavior, of course. So the solution is dependent on the source never being updated except for when a user actually toggles an expander.
Thus, I suspect the problem is that the initialization of the controls is somehow triggering a source update. Even if Expander 1 was correctly initialized as expanded, it would be reset when the bindings were refreshed on any of the other expanders.
To make ConvertBack
correct, it would need to be aware of the other expanders: It should only return null
if all of them are collapsed. I don't see a clean way of handling this from within a converter, though. Perhaps the best solution then would be to use a one-way binding (no ConvertBack
) and handle the Expanded and Collapsed events this way or similar (where _expanders
is a list of all of the 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'm using Tag for the identifier used in the viewmodel.
EDIT:
To solve it in a more "MVVM" way, you could 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.
}
Store the collection in ExpanderListViewModel
and add PropertyChanged handlers for each one at 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 a different item of 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 may also want to look into defining a custom ItemsControl to dynamically generate the Expanders based on the collection.)
In this case the SelectedExpander
property would no longer be needed, but it could be implemented this way:
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 would be equivalent ways of initializing the first expander:
viewModel.SelectedExpander = viewModel.Expanders[0];
viewModel.Expanders[0].IsSelected = true;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With