【问题标题】:Collapse all the expanders and expand one of them by default折叠所有扩展器并默认展开其中一个
【发布时间】:2014-02-13 17:41:03
【问题描述】:

我有多个扩展器,并且我正在寻找一种方法来在其中一个扩展器展开时折叠所有其他扩展器。我找到了这个解决方案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>

转换器:

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

它工作正常,但现在我想在应用程序启动时扩展其中一个扩展器!

我已经尝试将值(1、2 或 3)放在 SelectedExpander 属性中,但默认情况下没有扩展器被扩展!

如何将这种可能性添加到我的扩展器中?

【问题讨论】:

  • 绑定模式改成OneWay,初始化是否有效?如果 IsExpanded 在扩展器自身初始化期间被显式设置为 false,它将重置您的 viewmodel 属性。
  • 当我将模式更改为 OneWay 时,在应用程序启动时扩展器会被扩展,但是当我点击另一个时,第一个扩展器仍然是扩展的 :(

标签: c# wpf mvvm expander


【解决方案1】:

考虑一下如果您在 Expander 2 上调用 UpdateSource 而 Expander 1 被选中会发生什么:

  • ConvertBack 以当前 IsExpanded 值 (false) 为 Expander 2 调用,并返回 null
  • SelectedExpander 更新为 null
  • Convert 被所有其他扩展器调用,因为 SelectedExpander 已更改,导致所有其他 IsExpanded 值也设置为 false

当然,这不是正确的行为。因此,解决方案依赖于永远不会更新的源,除非用户实际切换扩展器。

因此,我怀疑问题在于控件的初始化以某种方式触发了源更新。即使扩展器 1 被正确初始化为扩展器,它也会在任何其他扩展器上刷新绑定时重置。

要使ConvertBack 正确,它需要注意其他扩展器:如果所有 都折叠,它应该只返回null。不过,我没有看到从转换器内部处理这个问题的干净方法。那么最好的解决方案可能是使用单向绑定(没有ConvertBack)并以这种方式或类似方式处理ExpandedCollapsed 事件(其中_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;
    }
}

在这种情况下,我使用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 以根据集合动态生成扩展器。)

在这种情况下,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;

【讨论】:

  • 谢谢,它破坏了 MVVM,但我不得不使用这个解决方案。
【解决方案2】:

改变Convert方法(给定here)内容如下

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

由于与 == 运算符进行对象比较,以前的内容无法正常工作。

【讨论】:

  • 我这样做了,并将值 1 放在 SelectedExpander 中,但在应用启动时没有得到“Expander 1”来获取扩展器 :(
  • 在何处以及如何将值设置为 SelectedExpander?
  • 我已经在ViewModel中实现了INotifyPropertyChanged,在SelectedExpander中添加了OnPropertyChanged。我通过添加断点对其进行了测试,并且 ViewModel 中的值发生了更改,但 View 没有得到更改!
  • 对我来说,只有转换器更改才能正常工作。甚至没有添加 INotifyPropertyChanged。
  • 没有 INotifyPropertyChanged 就无法使用 ot
【解决方案3】:

我只用您的代码创建了一个 WPF 项目,将 StackPanel 作为 MainWindow 的内容并在 MainWindow() 中调用 InitializeComponent() 后调用您的 初始化 代码,工作方式类似于只需删除即可获得魅力

return (value == parameter);

来自您的ExpanderToBooleanConverter.Convert。实际上@Boopesh answer 也可以。即使你这样做了

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

它有效,但在这种情况下,SelectedExpander 仅支持字符串值。

我建议您在Convert 中再次尝试其他返回,如果它不起作用,您的问题可能出在您的初始化代码中。您可能在组件正确初始化之前设置了SelectedExpander

【讨论】:

  • nmclean 的 commebt 帮助。我不得不将模式更改为“OneWay”,它起作用了,但我不知道为什么!
  • 编辑:它不起作用,因为现在,当我点击另一个扩展器时,打开的扩展器不会折叠:(
  • @Schneider 你需要TwoWay 以便相应地执行Converter。你有关于SelectedExpander 的任何其他逻辑(例如处理它的OnPropertyChanged 事件)?
  • @Schneider 更改为OneWay 只是为了缩小问题范围。当用户单击扩展器时,您仍然需要 TwoWay 绑定来更新视图模型。
【解决方案4】:

我已经编写了一个示例代码来演示如何实现你想要的。

<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 标记扩展。

你可以找到例子here

【讨论】:

    【解决方案5】:

    视图为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>
    

    代码隐藏

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

    IValue 转换器

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

      我是这样做的

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-03-29
        • 2017-09-29
        • 2013-09-20
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多