Consider what would happen if you called UpdateSource on Expander 2 while Expander 1 is selected:
ConvertBack
is called for Expander 2 with its currentIsExpanded
value (false
), and returnsnull
.SelectedExpander
is updated tonull
.Convert
is called for all other expanders, becauseSelectedExpander
changed, causing all the otherIsExpanded
values to be set tofalse
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;