views:

2846

answers:

1

I'm tracking ListView selection changes in an MVVM design by binding to IsSelected. I also need to track the current item by enabling IsSynchronizedWithCurrentItem.

I find that when I have two ListView binding to the same collection I get the InvalidOperationException: "Collection was modified; enumeration operation may not execute." It seems to be a synchonization error between the two ListViews; one is triggering a PropertyChanged event while the other is updating the Selector perhaps?

I can't figure out how to get around this other than forgoing use of IsSynchronizedWithCurrentItem and managing it myself. Any ideas?

Thanks.

The ViewModel and code behind:

public class Item : INotifyPropertyChanged
{        
    public string Name{ get; set; }

    public bool IsSelected
    {
        get { return isSelected; }
        set { isSelected = value; OnPropertyChanged("IsSelected"); }
    }
    private bool isSelected;

    public event PropertyChangedEventHandler PropertyChanged;
    private void OnPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

public class ViewModel
{
    public ViewModel()
    {
        Items = new ObservableCollection<Item>()
                {
                    new Item(){Name = "Foo"},
                    new Item(){Name = "Bar"}
                };
    }
    public ObservableCollection<Item> Items { get; private set; }
}

public partial class Window1 : Window
{
    public Window1()
    {
        InitializeComponent();
        DataContext = new ViewModel();
    }
}

The XAML:

<Window x:Class="WpfApplication1.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="100" Width="100">
    <StackPanel>
        <ListView DataContext="{Binding Items}" ItemsSource="{Binding}" 
                  IsSynchronizedWithCurrentItem="True" SelectionMode="Single">
            <ListView.ItemContainerStyle>
                <Style TargetType="ListViewItem">
                    <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
                </Style>
            </ListView.ItemContainerStyle>
            <ListView.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Path=Name, Mode=OneWay}"/>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
        <ListView DataContext="{Binding Items}" ItemsSource="{Binding}" 
              IsSynchronizedWithCurrentItem="True" SelectionMode="Single">
            <ListView.ItemContainerStyle>
                <Style TargetType="ListViewItem">
                    <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
                </Style>
            </ListView.ItemContainerStyle>
            <ListView.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Path=Name, Mode=OneWay}"/>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </StackPanel>
</Window>
+2  A: 

I cannot offer a direct fix for your problem. However, I do have a solution that will work.

What you can do is introduce a second property on your View Model called 'SelectedItem' that will hold a reference to the Item that is selected in your ListView. In addition, in your View Model you listen for the PropertyChanged event. If the associated Property Name is IsSelected then you update the SelectedItem property to be the sender of that event (the Item that now has IsSelected = true). You can then bind the SelectedItem property of the ListView to the property of the same name of the ViewModel class.

My code for the revised ViewModel class is below.

public class ViewModel : INotifyPropertyChanged
{
    private Item _selectedItem;

    public ViewModel()
    {
        Items = new ObservableCollection<Item>()
            {
                new Item {Name = "Foo"},
                new Item {Name = "Bar"}
            };

        foreach ( Item anItem in Items )
        {
            anItem.PropertyChanged += OnItemIsSelectedChanged;
        }
    }

    public ObservableCollection<Item> Items { get; private set; }

    public Item SelectedItem
    {
        get { return _selectedItem; }
        set
        {
            // only update if the value is difference, don't
            // want to send false positives
            if ( _selectedItem == value )
            {
                return;
            }

            _selectedItem = value;
            OnPropertyChanged("SelectedItem");
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnItemIsSelectedChanged(object sender, PropertyChangedEventArgs e)
    {
        if ( e.PropertyName != "IsSelected" )
        {
            return;
        }

        SelectedItem = sender as Item;
    }

    private void OnPropertyChanged(string propertyName)
    {
        if ( PropertyChanged != null )
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}
Richard C. McGuire
Yes, indeed this is just how I'm working around the problem for now. I would add that for me, it's necessary to monitor the CollectionChanged event on Items to add/remove PropertyChanged registration as the collection changes.
Terrence
@terrence you should accept his answer if this is how its working :) did u find a better way in the last year?
Simon_Weaver