问题
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
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'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 ?
回答1:
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;
回答2:
Change the Convert method (given here) content as follows
if (value == null)
return false;
return (value.ToString() == parameter.ToString());
Previous content not working because of object comparison with == operator.
回答3:
I created an WPF project with just your code, having the StackPanel
as the content of the MainWindow
and invoking your Initialization code after calling InitializeComponent()
in MainWindow()
and works like a charm by simply removing
return (value == parameter);
from your ExpanderToBooleanConverter.Convert
. Actually @Boopesh answer works too. Even if you do
return ((string)value == (string)parameter);
it works, but in that case only string values are supported for SelectedExpander
.
I'd suggest you to try again those other returns in your Convert
and if it doesn't work, your problem may be in your initialization code. It is possible that you are setting SelectedExpander
before the components have been properly initialized.
回答4:
I have wrote an example code which demonstrate how to achive 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 so:
public class Model
{
public string Header { get; set; }
public string Content { get; set; }
}
And the ViewModel expose 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 dont wont to create a view model (Maybe this is a static) you can use the x:Array markup extension.
you can find example here
回答5:
You need to set the property after the view is Loaded
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;
}
}
回答6:
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;
}
}
}
来源:https://stackoverflow.com/questions/21303697/collapse-all-the-expanders-and-expand-one-of-them-by-default