Свернуть все расширители и по умолчанию развернуть один из них

У меня несколько расширителей, и я искал способ свернуть все остальные расширители, когда один из них раскрывается. И я нашел это решение здесь

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>

Конвертер:

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));
        }
    }
}

Инициализация

var viewModel = new ExpanderListViewModel();
StackPanel1.DataContext = viewModel;
viewModel.SelectedExpander = 1;

// I tried this also
viewModel.SelectedExpander = "1";

Работает нормально, но теперь хочу развернуть один из расширителей при запуске приложения!

Я уже пытался поместить значения (1, 2 или 3) в свойство SelectedExpander, но по умолчанию ни один из расширителей не расширяется!

Как я могу добавить эту возможность в мои расширители?


person Wassim AZIRAR    schedule 23.01.2014    source источник
comment
Инициализация работает, если изменить режим привязки на OneWay? Если IsExpanded явно установлен в false во время собственной инициализации расширителя, это сбросит ваше свойство viewmodel.   -  person nmclean    schedule 27.01.2014
comment
Когда я меняю режим на OneWay, при запуске приложения Expander расширяется, но когда я нажимаю на другой, первым, когда он все еще расширяется :(   -  person Wassim AZIRAR    schedule 27.01.2014


Ответы (6)


Подумайте, что произойдет, если вы вызовете UpdateSource на расширителе 2, когда выбран расширитель 1:

  • ConvertBack вызывается для Expander 2 с его текущим значением IsExpanded (false) и возвращает null.
  • SelectedExpander обновляется до null.
  • Convert вызывается для всех других расширителей, потому что SelectedExpander изменился, в результате чего все остальные IsExpanded значения также будут установлены на false.

Конечно, это неправильное поведение. Таким образом, решение зависит от того, что источник никогда не обновляется, за исключением случаев, когда пользователь фактически переключает расширитель.

Таким образом, я подозреваю, что проблема в том, что инициализация элементов управления каким-то образом запускает обновление источника. Даже если Expander 1 был правильно инициализирован как расширенный, он будет сброшен при обновлении привязок на любом из других модулей расширения.

Чтобы сделать ConvertBack правильным, ему необходимо знать о других модулях расширения: он должен возвращать null только в том случае, если все из них свернуты. Однако я не вижу чистого способа справиться с этим из конвертера. Возможно, лучшим решением было бы использовать одностороннюю привязку (без ConvertBack) и обрабатывать Expanded и Collapsed событиями таким или подобным образом (где _expanders - это список всех элементов управления расширением):

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;
    }
}

В этом случае я использую Тег для идентификатора, используемого в модели просмотра.

РЕДАКТИРОВАТЬ:

Чтобы решить эту проблему более "MVVM" способом, у вас может быть набор моделей представления для каждого расширителя с отдельным свойством для привязки IsExpanded к:

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

Сохраните коллекцию в ExpanderListViewModel и добавьте обработчики PropertyChanged для каждого из них при инициализации:

// 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;
            }
        }
    }
}

Затем привяжите каждый расширитель к другому элементу коллекции Expanders:

<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>

(Вы также можете изучить возможность определения пользовательского ItemsControl для динамического создания Expanders на основе коллекции.)

В этом случае свойство SelectedExpander больше не понадобится, но его можно реализовать следующим образом:

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");
    }
}

И обновите указанный выше обработчик PropertyChanged как:

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

Итак, теперь эти две строки будут эквивалентными способами инициализации первого расширителя:

viewModel.SelectedExpander = viewModel.Expanders[0];
viewModel.Expanders[0].IsSelected = true;
person nmclean    schedule 27.01.2014
comment
Спасибо, это нарушает работу MVVM, но я был вынужден использовать это решение. - person Wassim AZIRAR; 29.01.2014

Измените метод преобразования (см. здесь) содержание следующим образом

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

Предыдущий контент не работает из-за сравнения объекта с оператором ==.

person Boopesh    schedule 23.01.2014
comment
Я сделал это и поместил значение 1 в SelectedExpander, но не получил Expander 1 для получения экспандера при запуске приложения :( - person Wassim AZIRAR; 23.01.2014
comment
где и как установить значение SelectedExpander? - person Boopesh; 23.01.2014
comment
Я уже реализовал INotifyPropertyChanged в ViewModel и добавил OnPropertyChanged в SelectedExpander. Я протестировал это, добавив точку останова, и значение изменилось в ViewModel, но View не получил изменений! - person Wassim AZIRAR; 23.01.2014
comment
Для меня он работает так, как вы ожидали, только с изменениями Конвертера. Даже не добавил INotifyPropertyChanged. - person Boopesh; 23.01.2014
comment
Нет работы с ot без INotifyPropertyChanged - person Wassim AZIRAR; 23.01.2014

Я создал проект WPF только с вашим кодом, используя StackPanel в качестве содержимого MainWindow и вызвав ваш код Initialization после вызова InitializeComponent() в MainWindow(), и работает как шарм, просто удаляя

return (value == parameter);

с вашего ExpanderToBooleanConverter.Convert. На самом деле @Boopesh answer тоже работает. Даже если ты это сделаешь

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

он работает, но в этом случае для SelectedExpander поддерживаются только строковые значения.

Я бы посоветовал вам снова попробовать эти другие возвраты в вашем Convert, и если это не сработает, ваша проблема может быть в вашем коде инициализации. Возможно, вы устанавливаете SelectedExpander до того, как компоненты будут правильно инициализированы.

person jnovo    schedule 27.01.2014
comment
Комбат nmclean помог. Мне пришлось изменить режим на OneWay, это сработало, но я не знаю почему! - person Wassim AZIRAR; 27.01.2014
comment
Изменить: это не работает, потому что теперь, когда я нажимаю на другой Expander, открытый не сворачивается :( - person Wassim AZIRAR; 27.01.2014
comment
@Schneider вам нужен TwoWay, чтобы Converter выполнялся соответственно. Есть ли у вас какая-либо другая логика относительно SelectedExpander (например, обработка его события OnPropertyChanged)? - person jnovo; 27.01.2014
comment
@Schneider Переход на OneWay был только для того, чтобы сузить проблему. Вам по-прежнему понадобится TwoWay привязка для обновления модели просмотра, когда пользователь щелкает расширитель. - person nmclean; 27.01.2014

Я написал пример кода, демонстрирующий, как достичь желаемого.

<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>

Модель выглядит так:

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

И ViewModel предоставляет модель представлению:

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;
        }
    }

Если вы не хотите создавать модель представления (возможно, это статическая), вы можете использовать расширение разметки x: Array.

вы можете найти пример здесь

person Moran Moshe    schedule 27.01.2014

Вам необходимо установить свойство после того, как представление будет Загружено

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>

Код позади

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;
    }
}
person WiiMaxx    schedule 28.01.2014

Я сделал это вот так

<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;
        }
    }
}
person Carl Rocco    schedule 23.11.2014