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

时间:2022-08-02 16:01:18

I am making a WPF application that is navigable via custom "Next" and "Back" buttons and commands (i.e. not using a NavigationWindow). On one screen, I have a ListBox that has to support multiple selections (using the Extended mode). I have a view model for this screen and store the selected items as a property, since they need to be maintained.

我正在制作一个WPF应用程序,可以通过自定义的“Next”和“Back”按钮和命令(例如,不使用NavigationWindow)导航。在一个屏幕上,我有一个列表框,它必须支持多个选择(使用扩展模式)。我有这个屏幕的视图模型,并将所选项作为属性存储,因为它们需要维护。

However, I am aware that the SelectedItems property of a ListBox is read-only. I have been trying to work around the issue using this solution here, but I have not been able to adopt it into my implementation. I found that I can't differentiate between when one or more elements are deselected and when I navigate between screens (NotifyCollectionChangedAction.Remove is raised in both cases, since technically all the selected items are deselected when navigating away from the screen). My navigation commands are located in a separate view model which manages the view models for each screen, so I can't put any implementation related to the view model with the ListBox in there.

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

I have found several other less elegant solutions, but none of these seem to enforce a two-way binding between the view model and the view.

我还发现了其他一些不那么优雅的解决方案,但它们似乎都没有在视图模型和视图之间强制执行双向绑定。

Any help would be greatly appreciated. I can provide some of my source code if it would help to understand my problem.

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

6 个解决方案

#1


44  

Try creating an IsSelected property on each of your data items and binding ListBoxItem.IsSelected to that property

尝试在每个数据项和绑定ListBoxItem上创建一个IsSelected属性。IsSelected,财产

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

#2


16  

Rachel's solutions works great! But there is one problem I've encountered - if you override the style of ListBoxItem, you loose the original styling applied to it (in my case responsible for highlighting the selected item etc.). You can avoid this by inheriting from the original style:

瑞秋的解决方案工作好了!但是我遇到了一个问题——如果您覆盖了ListBoxItem的样式,您就会丢失应用于它的原始样式(在我的案例中,负责突出显示所选择的项目等)。你可以通过继承原始风格来避免这一点:

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

Note setting BasedOn (see this answer) .

注意设置BasedOn(见此答案)。

#3


8  

I couldn't get Rachel's solution to work how I wanted it, but I found Sandesh's answer of creating a custom dependency property to work perfectly for me. I just had to write similar code for a ListBox:

我无法让Rachel的解决方案按照我希望的方式运行,但我找到了Sandesh的答案,即创建一个自定义依赖属性,以完美地为我工作。我只需要为列表框编写类似的代码:

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

}

In my View Model I just referenced that property to get my selected list.

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

#4


1  

I kept looking into an easy solution for this but with no luck.

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

The solution Rachel has is good if you already have the Selected property on the object within your ItemsSource. If you do not, you have to create a Model for that business model.

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

I went a different route. A quick one, but not perfect.

我走了另一条路。一个快速的,但不是完美的。

On your ListBox create an event for SelectionChanged.

在您的列表框中创建一个事件用于选择更改。

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

Now implement the event on the code behind of your XAML page.

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

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

Tada. Done.

这样。完成了。

This was done with the help of converting SelectedItemCollection to a List.

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

#5


0  

Not satisfied with the given answers I was trying to find one by myself... Well it turns out to be more like a hack then a solution but for me that works fine. This Solution uses MultiBindings in a special way. First it may look like a ton of Code but you can reuse it with very little effort.

不满意给出的答案,我试图自己找到一个……结果它更像是一种破解,而不是一种解决方案,但对我来说,这是可行的。这个解决方案以一种特殊的方式使用多绑定。首先,它可能看起来像大量的代码,但是您可以非常轻松地重用它。

First I implemented a 'IMultiValueConverter'

首先,我实现了一个“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 };
    }
}

And a SelectedItems Container/Wrapper:

和设置selecteditem容器/包装:

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

Now we create the Binding for our ListBox.SelectedItem (Singular). Note: You have to create a static Resource for the 'Converter'. This may be done once per application and be reused for all ListBoxes that need the converter.

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

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

In the ViewModel I created the Container where I can bind to. It is important to initialize it with new() in order to fill it with the values.

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

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

And that's it. Maybe someone sees some improvements? What do You think about it?

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

#6


-1  

Turns out binding a check box to the IsSelected property and putting the textblock and checkbox within a stack panel does the trick!

将复选框绑定到IsSelected属性,并将textblock和checkbox放在一个堆栈面板中,就可以达到这个目的!

#1


44  

Try creating an IsSelected property on each of your data items and binding ListBoxItem.IsSelected to that property

尝试在每个数据项和绑定ListBoxItem上创建一个IsSelected属性。IsSelected,财产

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

#2


16  

Rachel's solutions works great! But there is one problem I've encountered - if you override the style of ListBoxItem, you loose the original styling applied to it (in my case responsible for highlighting the selected item etc.). You can avoid this by inheriting from the original style:

瑞秋的解决方案工作好了!但是我遇到了一个问题——如果您覆盖了ListBoxItem的样式,您就会丢失应用于它的原始样式(在我的案例中,负责突出显示所选择的项目等)。你可以通过继承原始风格来避免这一点:

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

Note setting BasedOn (see this answer) .

注意设置BasedOn(见此答案)。

#3


8  

I couldn't get Rachel's solution to work how I wanted it, but I found Sandesh's answer of creating a custom dependency property to work perfectly for me. I just had to write similar code for a ListBox:

我无法让Rachel的解决方案按照我希望的方式运行,但我找到了Sandesh的答案,即创建一个自定义依赖属性,以完美地为我工作。我只需要为列表框编写类似的代码:

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

}

In my View Model I just referenced that property to get my selected list.

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

#4


1  

I kept looking into an easy solution for this but with no luck.

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

The solution Rachel has is good if you already have the Selected property on the object within your ItemsSource. If you do not, you have to create a Model for that business model.

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

I went a different route. A quick one, but not perfect.

我走了另一条路。一个快速的,但不是完美的。

On your ListBox create an event for SelectionChanged.

在您的列表框中创建一个事件用于选择更改。

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

Now implement the event on the code behind of your XAML page.

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

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

Tada. Done.

这样。完成了。

This was done with the help of converting SelectedItemCollection to a List.

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

#5


0  

Not satisfied with the given answers I was trying to find one by myself... Well it turns out to be more like a hack then a solution but for me that works fine. This Solution uses MultiBindings in a special way. First it may look like a ton of Code but you can reuse it with very little effort.

不满意给出的答案,我试图自己找到一个……结果它更像是一种破解,而不是一种解决方案,但对我来说,这是可行的。这个解决方案以一种特殊的方式使用多绑定。首先,它可能看起来像大量的代码,但是您可以非常轻松地重用它。

First I implemented a 'IMultiValueConverter'

首先,我实现了一个“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 };
    }
}

And a SelectedItems Container/Wrapper:

和设置selecteditem容器/包装:

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

Now we create the Binding for our ListBox.SelectedItem (Singular). Note: You have to create a static Resource for the 'Converter'. This may be done once per application and be reused for all ListBoxes that need the converter.

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

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

In the ViewModel I created the Container where I can bind to. It is important to initialize it with new() in order to fill it with the values.

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

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

And that's it. Maybe someone sees some improvements? What do You think about it?

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

#6


-1  

Turns out binding a check box to the IsSelected property and putting the textblock and checkbox within a stack panel does the trick!

将复选框绑定到IsSelected属性,并将textblock和checkbox放在一个堆栈面板中,就可以达到这个目的!