如何在可导航的应用程序中支持ListBox SelectedItems与MVVM的绑定

时间:2012-06-21 16:52:18

标签: c# wpf mvvm listbox

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

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

我找到了其他一些不太优雅的解决方案,但这些解决方案似乎都没有强制实施视图模型和视图之间的双向绑定。

非常感谢任何帮助。如果有助于理解我的问题,我可以提供一些源代码。

9 个答案:

答案 0 :(得分:47)

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

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

答案 1 :(得分:18)

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

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

注意设置BasedOn(请参阅this answer

答案 2 :(得分:8)

我无法让Rachel的解决方案按照我想要的方式工作,但我找到Sandesh's创建自定义dependency property的答案,以便为我完美工作。我只需要为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("SelectedItemsList", typeof(IList), typeof(ListBoxCustom), new PropertyMetadata(null));

}

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

答案 3 :(得分:3)

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

如果您已在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();
}

多田。完成。

这是在converting SelectedItemCollection to a List的帮助下完成的。

答案 4 :(得分:0)

对我给出的答案不满意我试图自己找一个...... 好吧事实证明它更像是一个黑客然后一个解决方案,但对我来说工作正常。此解决方案以特殊方式使用MultiBindings。 首先,它可能看起来像一大堆代码,但您可以轻松地重复使用它。

首先,我实施了一个&#39; IMultiValueConverter&#39;

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(Singular)创建Binding。注意:您必须为“转换器”创建静态资源。这可以在每个应用程序中完成一次,并且可以重用于需要转换器的所有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");
            }
        }
    }

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

答案 5 :(得分:0)

这是另一个解决方案。它与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"
    />

有一件事需要提防。就我而言,ListBox可能会从视图中删除。由于某些原因,这导致SelectedItems属性更改为空列表。反过来,这导致视图模型的属性更改为空列表。根据您的用例,可能不希望如此。

答案 6 :(得分: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}"
/>

它尚未经过全面测试,但已通过乍看检查。我试图通过在集合上使用动态类型来使其可重用。

答案 7 :(得分:0)

使用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}" />

ViewModel:

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

答案 8 :(得分:-1)

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