41

我正在制作一个可通过自定义“下一步”和“后退”按钮和命令(即不使用NavigationWindow)导航的 WPF 应用程序。在一个屏幕上,我有一个ListBox必须支持多项选择(使用Extended模式)。我有这个屏幕的视图模型并将所选项目存储为属性,因为它们需要维护。

但是,我知道 a 的SelectedItems属性ListBox是只读的。我一直在尝试使用这里的解决方案来解决这个问题,但我无法将它应用到我的实现中。我发现我无法区分何时取消选择一个或多个元素以及何时在屏幕之间导航(NotifyCollectionChangedAction.Remove在这两种情况下都会引发,因为从技术上讲,当导航离开屏幕时所有选定的项目都被取消选择)。我的导航命令位于一个单独的视图模型中,该模型管理每个屏幕的视图模型,所以我不能在其中放置与视图模型相关的任何实现ListBox

我发现了其他几个不太优雅的解决方案,但这些似乎都没有在视图模型和视图之间强制执行双向绑定。

任何帮助将不胜感激。如果有助于理解我的问题,我可以提供一些源代码。

4

9 回答 9

57

尝试在每个数据项上创建一个IsSelected属性并绑定ListBoxItem.IsSelected到该属性

<Style TargetType="{x:Type ListBoxItem}">
    <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
于 2012-06-21T17:07:18.493 回答
20

Rachel 的解决方案效果很好!但是我遇到了一个问题——如果你覆盖 的样式ListBoxItem,你会失去应用到它的原始样式(在我的例子中负责突出显示选定的项目等)。您可以通过继承原始样式来避免这种情况:

<Style TargetType="{x:Type ListBoxItem}" BasedOn="{StaticResource {x:Type ListBoxItem}}">
    <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>

注意设置BasedOn(见这个答案)。

于 2014-01-21T15:55:23.813 回答
13

我无法让 Rachel 的解决方案按照我想要的方式工作,但我发现Sandesh 的创建自定义 依赖属性的答案对我来说非常适合。我只需要为 ListBox 编写类似的代码:

public class ListBoxCustom : ListBox
{
    public ListBoxCustom()
    {
        SelectionChanged += ListBoxCustom_SelectionChanged;
    }

    void ListBoxCustom_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        SelectedItemsList = SelectedItems;
    }

    public IList SelectedItemsList
    {
        get { return (IList)GetValue(SelectedItemsListProperty); }
        set { SetValue(SelectedItemsListProperty, value); }
    }

    public static readonly DependencyProperty SelectedItemsListProperty =
       DependencyProperty.Register(nameof(SelectedItemsList), typeof(IList), typeof(ListBoxCustom), new PropertyMetadata(null));

}

在我的视图模型中,我只是引用了该属性来获取我选择的列表。

于 2016-01-18T20:49:17.613 回答
6

我一直在寻找一个简单的解决方案,但没有运气。

如果您已经在 ItemsSource 中的对象上拥有 Selected 属性,那么 Rachel 的解决方案就很好。如果不这样做,则必须为该业务模型创建一个模型。

我走了一条不同的路线。一个快速的,但并不完美。

在您的 ListBox 上为 SelectionChanged 创建一个事件。

<ListBox ItemsSource="{Binding SomeItemsSource}"
         SelectionMode="Multiple"
         SelectionChanged="lstBox_OnSelectionChanged" />

现在在 XAML 页面后面的代码上实现事件。

private void lstBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
    var listSelectedItems = ((ListBox) sender).SelectedItems;
    ViewModel.YourListThatNeedsBinding = listSelectedItems.Cast<ObjectType>().ToList();
}

多田。完毕。

这是在将 SelectedItemCollection 转换为 List的帮助下完成的。

于 2017-09-25T13:38:35.550 回答
3

这是另一个解决方案。它类似于 Ben 的answer,但绑定有两种方式。诀窍是ListBox在绑定数据项更改时更新 的选定项。

public class MultipleSelectionListBox : ListBox
{
    public static readonly DependencyProperty BindableSelectedItemsProperty =
        DependencyProperty.Register("BindableSelectedItems",
            typeof(IEnumerable<string>), typeof(MultipleSelectionListBox),
            new FrameworkPropertyMetadata(default(IEnumerable<string>),
                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnBindableSelectedItemsChanged));

    public IEnumerable<string> BindableSelectedItems
    {
        get => (IEnumerable<string>)GetValue(BindableSelectedItemsProperty);
        set => SetValue(BindableSelectedItemsProperty, value);
    }

    protected override void OnSelectionChanged(SelectionChangedEventArgs e)
    {
        base.OnSelectionChanged(e);
        BindableSelectedItems = SelectedItems.Cast<string>();
    }

    private static void OnBindableSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is MultipleSelectionListBox listBox)
            listBox.SetSelectedItems(listBox.BindableSelectedItems);
    }
}

不幸的是,我无法IList用作 BindableSelectedItems 类型。这样做会发送null到我的视图模型的属性,其类型是IEnumerable<string>.

这是 XAML:

<v:MultipleSelectionListBox
    ItemsSource="{Binding AllMyItems}"
    BindableSelectedItems="{Binding MySelectedItems}"
    SelectionMode="Multiple"
    />

有一点需要注意。在我的情况下, aListBox可能会从视图中删除。由于某种原因,这会导致SelectedItems属性更改为空列表。反过来,这会导致视图模型的属性更改为空列表。根据您的用例,这可能是不可取的。

于 2018-07-09T22:39:45.210 回答
2

使用 Command 和 Interactivities EventTrigger 很容易做到这一点。如果您想要显示更新的计数,ItemsCount 只是用于 XAML 的绑定属性。

XAML:

     <ListBox ItemsSource="{Binding SomeItemsSource}"
                 SelectionMode="Multiple">
        <i:Interaction.Triggers>
         <i:EventTrigger EventName="SelectionChanged">
            <i:InvokeCommandAction Command="{Binding SelectionChangedCommand}" 
                                   CommandParameter="{Binding ElementName=MyView, Path=SelectedItems.Count}" />
         </i:EventTrigger>
        </Interaction.Triggers>    
    </ListView>

<Label Content="{Binding ItemsCount}" />

视图模型:

    private int _itemsCount;
    private RelayCommand<int> _selectionChangedCommand;

    public ICommand SelectionChangedCommand
    {
       get {
                return _selectionChangedCommand ?? (_selectionChangedCommand = 
             new RelayCommand<int>((itemsCount) => { ItemsCount = itemsCount; }));
           }
    }

        public int ItemsCount
        {
            get { return _itemsCount; }
            set { 
              _itemsCount = value;
              OnPropertyChanged("ItemsCount");
             }
        }
于 2018-11-27T21:52:12.500 回答
0

这对我来说是一个主要问题,我看到的一些答案要么太老套,要么需要重置SelectedItems属性值,破坏附加到属性 OnCollectionChanged 事件的任何代码。但我设法通过直接修改集合来获得一个可行的解决方案,作为奖励,它甚至支持SelectedValuePath对象集合。

public class MultipleSelectionListBox : ListBox
{
    internal bool processSelectionChanges = false;

    public static readonly DependencyProperty BindableSelectedItemsProperty =
        DependencyProperty.Register("BindableSelectedItems",
            typeof(object), typeof(MultipleSelectionListBox),
            new FrameworkPropertyMetadata(default(ICollection<object>),
                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnBindableSelectedItemsChanged));

    public dynamic BindableSelectedItems
    {
        get => GetValue(BindableSelectedItemsProperty);
        set => SetValue(BindableSelectedItemsProperty, value);
    }


    protected override void OnSelectionChanged(SelectionChangedEventArgs e)
    {
        base.OnSelectionChanged(e);

        if (BindableSelectedItems == null || !this.IsInitialized) return; //Handle pre initilized calls

        if (e.AddedItems.Count > 0)
            if (!string.IsNullOrWhiteSpace(SelectedValuePath))
            {
                foreach (var item in e.AddedItems)
                    if (!BindableSelectedItems.Contains((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null)))
                        BindableSelectedItems.Add((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null));
            }
            else
            {
                foreach (var item in e.AddedItems)
                    if (!BindableSelectedItems.Contains((dynamic)item))
                        BindableSelectedItems.Add((dynamic)item);
            }

        if (e.RemovedItems.Count > 0)
            if (!string.IsNullOrWhiteSpace(SelectedValuePath))
            {
                foreach (var item in e.RemovedItems)
                    if (BindableSelectedItems.Contains((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null)))
                        BindableSelectedItems.Remove((dynamic)item.GetType().GetProperty(SelectedValuePath).GetValue(item, null));
            }
            else
            {
                foreach (var item in e.RemovedItems)
                    if (BindableSelectedItems.Contains((dynamic)item))
                        BindableSelectedItems.Remove((dynamic)item);
            }
    }

    private static void OnBindableSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is MultipleSelectionListBox listBox)
        {
            List<dynamic> newSelection = new List<dynamic>();
            if (!string.IsNullOrWhiteSpace(listBox.SelectedValuePath))
                foreach (var item in listBox.BindableSelectedItems)
                {
                    foreach (var lbItem in listBox.Items)
                    {
                        var lbItemValue = lbItem.GetType().GetProperty(listBox.SelectedValuePath).GetValue(lbItem, null);
                        if ((dynamic)lbItemValue == (dynamic)item)
                            newSelection.Add(lbItem);
                    }
                }
            else
                newSelection = listBox.BindableSelectedItems as List<dynamic>;

            listBox.SetSelectedItems(newSelection);
        }
    }
}

绑定就像您期望 MS 自己完成的那样工作:

<uc:MultipleSelectionListBox 
    ItemsSource="{Binding Items}" 
    SelectionMode="Extended" 
    SelectedValuePath="id" 
    BindableSelectedItems="{Binding mySelection}"
/>

它尚未经过彻底测试,但已通过第一眼检查。我试图通过在集合上使用动态类型来保持它的可重用性。

于 2018-08-13T23:06:08.230 回答
0

结果证明将复选框绑定到 IsSelected 属性并将文本块和复选框放在堆栈面板中就可以了!

于 2017-04-21T17:46:44.633 回答
0

对我试图自己找到的给定答案不满意......好吧,事实证明它更像是一个黑客而不是一个解决方案,但对我来说效果很好。此解决方案以特殊方式使用 MultiBindings。首先,它可能看起来像大量代码,但您可以毫不费力地重用它。

首先我实现了一个'IMultiValueConverter'

public class SelectedItemsMerger : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        SelectedItemsContainer sic = values[1] as SelectedItemsContainer;

        if (sic != null)
            sic.SelectedItems = values[0];

        return values[0];
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        return new[] { value };
    }
}

还有一个 SelectedItems 容器/包装器:

public class SelectedItemsContainer
{
    /// Nothing special here...
    public object SelectedItems { get; set; }
}

现在我们为 ListBox.SelectedItem(单数)创建绑定。注意:您必须为“转换器”创建一个静态资源。这可以在每个应用程序中完成一次,并被所有需要转换器的 ListBox 重用。

<ListBox.SelectedItem>
 <MultiBinding Converter="{StaticResource SelectedItemsMerger}">
  <Binding Mode="OneWay" RelativeSource="{RelativeSource Self}" Path="SelectedItems"/>
  <Binding Path="SelectionContainer"/>
 </MultiBinding>
</ListBox.SelectedItem>

在 ViewModel 中,我创建了可以绑定到的 Container。使用 new() 初始化它以便用值填充它是很重要的。

    SelectedItemsContainer selectionContainer = new SelectedItemsContainer();
    public SelectedItemsContainer SelectionContainer
    {
        get { return this.selectionContainer; }
        set
        {
            if (this.selectionContainer != value)
            {
                this.selectionContainer = value;
                this.OnPropertyChanged("SelectionContainer");
            }
        }
    }

就是这样。也许有人看到了一些改进?你怎么看待这件事?

于 2017-09-26T08:40:44.383 回答