locked
Binding to ListView.SelectedItems not working properly - is this a WPF bug? RRS feed

  • 問題

  • Hi,

    for my master/detail scenario, a user can select multiple items from a ListView, and then in the PropertiesPanel below, the properties of the (multiple) selected items should be shown.

    I made a PropertiesPanel user control that exposes a Subjects property through a SubjectsProperty dependency property. And I am binding that dependency property of the PropertiesPanel to the SelectedItems dependency property of the ListView in my Window.

    The problem is that when the selection changes in the ListView, the Subjects property of my panel does not get updated even though it is bound to the SelectItems property of the ListView.

    I then tried without that UserControl to figure out what was going on: I bound the Text property of a TextBox to the SelectedItems property of the ListView (using an IValueConverter to convert it into text), and what do you know: even that doesn't work! It seems like the binding with SelectedItems just doesn't work at all (binding with SelectedItem does work, for a single selection, but that's not what I need).

    Surely, this is not an exotic usage scenario at all, and others must have encountered this problem as well, right?

    I have a very simple example project exhibiting the problem here:

    http://users.skynet.be/koentanghe/Koen/Temp/ListViewSelectedItemsBinding.zip

     

    As you can see, I tried to work around this problem by making a new dependecy property called "Selection" on my Window, and then handling the SelectionChanged event explicitely by setting my Selection property first to null and then to listView.SelectedItems. That seems to help, but it is clearly a workaround, and I would like to know how to do this *properly*.

    Anyone?

    Thanks,

    Koen

     

    2008年3月26日 下午 12:58

解答

  • Handling SelectionChanged event in this scenarion seems to be the only solution.

     

    Through the lifetime of a ListView object we have an unchangable value for it`s SelectedItems property — collection object. So, we can`t track selection change by binding to ListView`s SelectedItems property.

     

    But we can make SelectionChanged event handling more suitable for XAML usage without any codebehind for each particular case.

     

    Something like this:

     

    Code Snippet

    <Window x:Class="ListViewSelectedItemsBinding.Window1"

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:local="clr-namespace:ListViewSelectedItemsBinding"

    xmlns:w="clr-namespace:System.Windows.Workarounds"

    x:Name="root" Width="500" Height="700">

    <Window.Resources>

    <local:MyConverter x:Key="myConverter"/>

    </Window.Resources>

    <StackPanel>

    <ListView x:Name="listView" ItemsSource="{Binding}" SelectionMode="Extended"

    SelectionChanged="listView_SelectionChanged"

    w:ListView.HasBindableSelectedItems="True">

    <ListView.View>

    <GridView>

    <GridViewColumn Header="Artist" DisplayMemberBinding="{Binding Path=Artist}" />

    <GridViewColumn Header="Title" DisplayMemberBinding="{Binding Path=Title}" />

    <GridViewColumn Header="Genre" DisplayMemberBinding="{Binding Path=Genre}" />

    </GridView>

    </ListView.View>

    </ListView>

    <StackPanel Background="LightGreen">

    <DockPanel>

    <Label DockPanel.Dock="Left" Content="SelectedItems.Count = " />

    <TextBox Text="{Binding ElementName=listView, Path=SelectedItems.Count, Mode=OneWay}" IsReadOnly="True" />

    </DockPanel>

    <DockPanel>

    <Label DockPanel.Dock="Left" Content="SelectedItems = " />

    <TextBox Text="{Binding ElementName=listView, Path=SelectedItems, Mode=OneWay,

    Converter={StaticResource myConverter}, ConverterParameter='Artist'}" IsReadOnly="True" />

    </DockPanel>

    </StackPanel>

    <StackPanel Background="LightBlue">

    <DockPanel>

    <Label DockPanel.Dock="Left" Content="Selection.Count = " />

    <TextBox Text="{Binding ElementName=root, Path=Selection.Count, Mode=OneWay}" IsReadOnly="True" />

    </DockPanel>

    <DockPanel>

    <Label DockPanel.Dock="Left" Content="Selection = " />

    <TextBox Text="{Binding ElementName=root, Path=Selection, Mode=OneWay,

    Converter={StaticResource myConverter}, ConverterParameter='Artist'}" IsReadOnly="True" />

    </DockPanel>

    </StackPanel>

    <local:PropertiesPanel x:Name="propertiesPanel1" Background="LightGreen"

    Subjects="{Binding ElementName=listView, Path=SelectedItems, Mode=OneWay}" />

    <local:PropertiesPanel x:Name="propertiesPanel2" Background="LightBlue"

    Subjects="{Binding ElementName=root, Path=Selection, Mode=OneWay}" />

    <local:PropertiesPanel x:Name="propertiesPanel3" Background="LightPink"

    Subjects="{Binding ElementName=listView, Path=BindableSelectedItems}" />

    </StackPanel>

    </Window>

     

     

    and place SelectionChanged event handling code into separate application file:

     

    Code Snippet

    using System;

    using System.Collections;

    using System.Windows.Data;

    using System.Windows.Controls;

     

    namespace System.Windows.Workarounds

    {

     

    public static class ListView

    {

    public static readonly DependencyProperty HasBindableSelectedItemsProperty;

    public static readonly DependencyProperty BindableSelectedItemsProperty;

    static DependencyProperty SelectionChangedHandlerProperty;

     

    static ListView()

    {

    BindableSelectedItemsProperty = DependencyProperty.Register(

    "BindableSelectedItems", typeof(IList),

    typeof(System.Windows.Controls.ListView));

    HasBindableSelectedItemsProperty = DependencyProperty.RegisterAttached(

    "HasBindableSelectedItems", typeof(bool),

    typeof(System.Windows.Controls.ListView), new PropertyMetadata(false));

    SelectionChangedHandlerProperty = DependencyProperty.RegisterAttached(

    "SelectionChangedHandler", typeof(SelectionChangedHandler),

    typeof(System.Windows.Controls.ListView));

    }

     

    public static void SetHasBindableSelectedItems(

    System.Windows.Controls.ListView source, bool value)

    {

    SelectionChangedHandler Handler = (SelectionChangedHandler)source.GetValue(SelectionChangedHandlerProperty);

    if (value && Handler == null)

    {

    Handler = new SelectionChangedHandler(source);

    source.SetValue(SelectionChangedHandlerProperty, Handler);

    } else if (!value && Handler != null)

    source.ClearValue(SelectionChangedHandlerProperty);

    }

    }

     

    internal class SelectionChangedHandler

    {

     

    Binding Binding;

     

    internal SelectionChangedHandler(System.Windows.Controls.ListView owner)

    {

    Binding = new Binding("SelectedItems");

    Binding.Source = owner;

    owner.SetBinding(ListView.BindableSelectedItemsProperty, Binding);

    owner.SelectionChanged +=

    new SelectionChangedEventHandler(Owner_SelectionChanged);

    }

     

    void Owner_SelectionChanged(object sender, SelectionChangedEventArgs e)

    {

    System.Windows.Controls.ListView Owner =

    (System.Windows.Controls.ListView)sender;

     

    BindingOperations.ClearBinding(

    Owner, ListView.BindableSelectedItemsProperty);

    Owner.SetBinding(ListView.BindableSelectedItemsProperty, Binding);

    }

    }

     

    }

     

     

     

    2008年3月27日 上午 06:48

所有回覆

  • Handling SelectionChanged event in this scenarion seems to be the only solution.

     

    Through the lifetime of a ListView object we have an unchangable value for it`s SelectedItems property — collection object. So, we can`t track selection change by binding to ListView`s SelectedItems property.

     

    But we can make SelectionChanged event handling more suitable for XAML usage without any codebehind for each particular case.

     

    Something like this:

     

    Code Snippet

    <Window x:Class="ListViewSelectedItemsBinding.Window1"

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:local="clr-namespace:ListViewSelectedItemsBinding"

    xmlns:w="clr-namespace:System.Windows.Workarounds"

    x:Name="root" Width="500" Height="700">

    <Window.Resources>

    <local:MyConverter x:Key="myConverter"/>

    </Window.Resources>

    <StackPanel>

    <ListView x:Name="listView" ItemsSource="{Binding}" SelectionMode="Extended"

    SelectionChanged="listView_SelectionChanged"

    w:ListView.HasBindableSelectedItems="True">

    <ListView.View>

    <GridView>

    <GridViewColumn Header="Artist" DisplayMemberBinding="{Binding Path=Artist}" />

    <GridViewColumn Header="Title" DisplayMemberBinding="{Binding Path=Title}" />

    <GridViewColumn Header="Genre" DisplayMemberBinding="{Binding Path=Genre}" />

    </GridView>

    </ListView.View>

    </ListView>

    <StackPanel Background="LightGreen">

    <DockPanel>

    <Label DockPanel.Dock="Left" Content="SelectedItems.Count = " />

    <TextBox Text="{Binding ElementName=listView, Path=SelectedItems.Count, Mode=OneWay}" IsReadOnly="True" />

    </DockPanel>

    <DockPanel>

    <Label DockPanel.Dock="Left" Content="SelectedItems = " />

    <TextBox Text="{Binding ElementName=listView, Path=SelectedItems, Mode=OneWay,

    Converter={StaticResource myConverter}, ConverterParameter='Artist'}" IsReadOnly="True" />

    </DockPanel>

    </StackPanel>

    <StackPanel Background="LightBlue">

    <DockPanel>

    <Label DockPanel.Dock="Left" Content="Selection.Count = " />

    <TextBox Text="{Binding ElementName=root, Path=Selection.Count, Mode=OneWay}" IsReadOnly="True" />

    </DockPanel>

    <DockPanel>

    <Label DockPanel.Dock="Left" Content="Selection = " />

    <TextBox Text="{Binding ElementName=root, Path=Selection, Mode=OneWay,

    Converter={StaticResource myConverter}, ConverterParameter='Artist'}" IsReadOnly="True" />

    </DockPanel>

    </StackPanel>

    <local:PropertiesPanel x:Name="propertiesPanel1" Background="LightGreen"

    Subjects="{Binding ElementName=listView, Path=SelectedItems, Mode=OneWay}" />

    <local:PropertiesPanel x:Name="propertiesPanel2" Background="LightBlue"

    Subjects="{Binding ElementName=root, Path=Selection, Mode=OneWay}" />

    <local:PropertiesPanel x:Name="propertiesPanel3" Background="LightPink"

    Subjects="{Binding ElementName=listView, Path=BindableSelectedItems}" />

    </StackPanel>

    </Window>

     

     

    and place SelectionChanged event handling code into separate application file:

     

    Code Snippet

    using System;

    using System.Collections;

    using System.Windows.Data;

    using System.Windows.Controls;

     

    namespace System.Windows.Workarounds

    {

     

    public static class ListView

    {

    public static readonly DependencyProperty HasBindableSelectedItemsProperty;

    public static readonly DependencyProperty BindableSelectedItemsProperty;

    static DependencyProperty SelectionChangedHandlerProperty;

     

    static ListView()

    {

    BindableSelectedItemsProperty = DependencyProperty.Register(

    "BindableSelectedItems", typeof(IList),

    typeof(System.Windows.Controls.ListView));

    HasBindableSelectedItemsProperty = DependencyProperty.RegisterAttached(

    "HasBindableSelectedItems", typeof(bool),

    typeof(System.Windows.Controls.ListView), new PropertyMetadata(false));

    SelectionChangedHandlerProperty = DependencyProperty.RegisterAttached(

    "SelectionChangedHandler", typeof(SelectionChangedHandler),

    typeof(System.Windows.Controls.ListView));

    }

     

    public static void SetHasBindableSelectedItems(

    System.Windows.Controls.ListView source, bool value)

    {

    SelectionChangedHandler Handler = (SelectionChangedHandler)source.GetValue(SelectionChangedHandlerProperty);

    if (value && Handler == null)

    {

    Handler = new SelectionChangedHandler(source);

    source.SetValue(SelectionChangedHandlerProperty, Handler);

    } else if (!value && Handler != null)

    source.ClearValue(SelectionChangedHandlerProperty);

    }

    }

     

    internal class SelectionChangedHandler

    {

     

    Binding Binding;

     

    internal SelectionChangedHandler(System.Windows.Controls.ListView owner)

    {

    Binding = new Binding("SelectedItems");

    Binding.Source = owner;

    owner.SetBinding(ListView.BindableSelectedItemsProperty, Binding);

    owner.SelectionChanged +=

    new SelectionChangedEventHandler(Owner_SelectionChanged);

    }

     

    void Owner_SelectionChanged(object sender, SelectionChangedEventArgs e)

    {

    System.Windows.Controls.ListView Owner =

    (System.Windows.Controls.ListView)sender;

     

    BindingOperations.ClearBinding(

    Owner, ListView.BindableSelectedItemsProperty);

    Owner.SetBinding(ListView.BindableSelectedItemsProperty, Binding);

    }

    }

     

    }

     

     

     

    2008年3月27日 上午 06:48
  • Since multiple selections are happening in the list box, the target panel should be geared to handle this and display the values accordingly. I don't think this is something that can be supported out of box, since in case of multiple selection, how would the framework know how do you want to merge the various values and display?

     

     

    2008年3月27日 上午 07:10
  • @Alexander:

     

    Thanks! I just tried it and it definitely works.

     

    So, if I understand correctly: the problem is really caused by the fact that the collection instance itself is not changed when the selection changes, although the content of the SelectedItems property does change. That's why I had to set my Selection property to null first and reset it to the SelectedItems immediately afterwards (causing two updates actually, which is not good...)

     

    And as for your workaround code: do I get it right that it works because you intercept a SelectionChanged event and in that handler you then clear and immediately reset the binding for the new BindableSelectedItems property? I checked and this way of doing it only introduces one "bound data update", so that is good. And it looks like I can still have my own SelectionChanged event handler in addition to yours.

    Are there any particular situations I need to be careful with when using your workaround class and properties? Or have you been using it yourself for sometime and found it to be a robust solution? (in any case it's better and more elegant than my workaround)

     

    Thanks again!

    Koen

     

    2008年3月27日 上午 11:36
  • @Atul:

    Sure, the panel must be able to handle a collection of selected items (just have a look at RealPlayer for example on how it can handle and display that). But that's not the point here: that's up to the panel to deal with.

    The problem was that the panel didn't even get notified when the selected items change through a binding (yes, the SelectionChanged event is fired and can be handled, but we were looking into using a binding here).

    2008年3月27日 上午 11:39
  •  KoenT wrote:

    I checked and this way of doing it only introduces one "bound data update", so that is good.

     

    Hmm, well, I need to correct myself here... This does also introduce 2 updates for the bound targets: one when clearing the binding, and another one when resetting the binding. So, I guess the bound targets will just have to be able to deal with that (getting a null value).

    Anyway, at least you don't need to write explicit SelectionChanged event handling code-behind, so that's still good.

    Koen

     

    2008年3月27日 下午 03:37
  • So I am trying to do this with an MVVM model with litle if any code-behind attached to the actual XAML view. (in our projects we have yet to have code behind on our xaml files)  Technical reasons asside, it definately is frustrating to have to break what feel like solid maintainable design patterns for a common user interface behavior.

    2009年4月22日 下午 07:31
  • Hi Alexander i have a question.

    I have a problem with a ListView, this ListView is Binded to an ObservableCollection wich in certain point has more than one element but when i try to change the selected item by using the mouse the SelecctionChanged is only fiered once and when i make an other selecction this event is not fiered but the view shows me this element like it was selected and also the other items i have selected previously. I was following this post but when i paste the code it doesn`t recognize the System:windows.Data and System.Windows.Controls. i can't find wich reference to add.

    2009年6月17日 下午 10:03