WPF problem: Binding BusinessListBase to ListView - strange delete behavior

WPF problem: Binding BusinessListBase to ListView - strange delete behavior

Old forum URL: forums.lhotka.net/forums/t/3235.aspx


wleary posted on Friday, July 20, 2007

I have been struggling with something now for quite a few hours. I have a WPF page that is bound to my CSLA object, for editing. I have a ListView that is bound to a property on the object, which is a BusinessListBase. I need to be able to delete items in this list. But, whenever I remove one, I see two items deleted from my ListView. Here are my business objects:

using System;
using System.Collections.Generic;
using System.Text;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using Csla;

namespace ListViewDeleteBug
{
    [Serializable]
    public class Roster : BusinessBase<Roster>
    {
        private Persons _persons;

        public Persons Persons
        {
            get { return _persons; }
        }

        public Roster()
        {
            _persons = new Persons();
            _persons.Add(new Person("Willa", "Cather"));
            _persons.Add(new Person("Isak", "Dinesen"));
            _persons.Add(new Person("Victor", "Hugo"));
            _persons.Add(new Person("Jules", "Verne"));

            _persons.ListChanged += new System.ComponentModel.ListChangedEventHandler(_persons_ListChanged);
        }

        protected override object GetIdValue()
        {
            return Guid.NewGuid();
        }

        void _persons_ListChanged(object sender, System.ComponentModel.ListChangedEventArgs e)
        {
            OnPropertyChanged(null);
        }
    }

    [Serializable]
    public class Persons : BusinessListBase<Persons, Person>
    {
    }

    [Serializable]
    public class Person : BusinessBase<Person>
    {
        private string firstName;
        private string lastName;

        public Person(string first, string last)
        {
            MarkAsChild();
            this.firstName = first;
            this.lastName = last;
        }

        public string FirstName
        {
            get { return firstName; }
            set { firstName = value; }
        }

        public string LastName
        {
            get { return lastName; }
            set { lastName = value; }
        }

        protected override object GetIdValue()
        {
            return firstName;
        }
    }
}

And here is the XAML and code-behind:

<Window x:Class="ListViewDeleteBug.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 xmlns:l="clr-namespace:ListViewDeleteBug"
    Title="ListViewDeleteBug" Height="300" Width="300"
    >
 <Grid>  
  <ListView x:Name="myNames" ItemsSource="{Binding Path=Persons,Mode=OneWay}" KeyDown="OnNamesKeyDown" SelectionMode="Single" IsSynchronizedWithCurrentItem="True">
   <ListView.View>
    <GridView>
     <GridViewColumn Header="Name" DisplayMemberBinding="{Binding FirstName}" Width="300"/>
    </GridView>
   </ListView.View>
  </ListView>
 </Grid>
</Window>

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;

namespace ListViewDeleteBug
{
    public partial class Window1 : System.Windows.Window
    {
        Roster _roster;

        public Window1()
        {
            InitializeComponent();

            _roster = new Roster();
            DataContext = _roster;
        }

        private void OnNamesKeyDown(object sender, KeyEventArgs args)
        {
            if (myNames.SelectedItem != null)
            {
                Person person = myNames.SelectedItem as Person;
                _roster.Persons.Remove(person);
            }

            args.Handled = true;
        }
    }
}

You'll see when you run this, that if you delete one of the names, two get deleted. I tried the code with an EditableRootList, and that did not exhibit the same behavior. Any help will be greatly appreciated. This has been driving me nuts.

RockfordLhotka replied on Friday, July 20, 2007

I don't know why, and I don't have time to keep digging, but OnNamesKeyDown() gets called twice when I hit the Delete key - which explains why 2 items get deleted Smile [:)]

wleary replied on Friday, July 20, 2007

Hi Rocky,

I am not seeing that behavior at all. I just tried with delete, and did not hit the event handler twice. I am also able to replicate the behavior simply by programmatically deleting the child. The problem goes away if I change the type of the Persons property to a non-BusinessListBase. Here is my new classes file:

using System;
using System.Collections.Generic;
using System.Text;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using Csla;

namespace ListViewDeleteBug
{

    [Serializable]
    public class Roster : BusinessBase<Roster>
    {
        private Persons _persons;

        public Persons Persons
        {
            get { return _persons; }
        }

        public Roster()
        {
            _persons = new Persons();
            _persons.Add(new Person("Willa", "Cather"));
            _persons.Add(new Person("Isak", "Dinesen"));
            _persons.Add(new Person("Victor", "Hugo"));
            _persons.Add(new Person("Jules", "Verne"));

            // THIS WORKS!!!
            _persons.CollectionChanged += new NotifyCollectionChangedEventHandler(_persons_CollectionChanged);
        }

        void _persons_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            OnPropertyChanged("Persons");
        }

        protected override object GetIdValue()
        {
            return Guid.NewGuid();
        }
    }

    [Serializable]
    public class Persons : List<Person>, INotifyCollectionChanged //BusinessListBase<Persons, Person>
    {
        #region INotifyCollectionChanged Members

        public event NotifyCollectionChangedEventHandler CollectionChanged;

        #endregion

        public void removeAtIndex(int index)
        {
            Person person = this[index];
            RemoveAt(index);
            CollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, person));
        }
    }

    [Serializable]
    public class Person : BusinessBase<Person>
    {
        private string firstName;
        private string lastName;

        public Person(string first, string last)
        {
            MarkAsChild();
            this.firstName = first;
            this.lastName = last;
        }

        public string FirstName
        {
            get { return firstName; }
            set { firstName = value; }
        }

        public string LastName
        {
            get { return lastName; }
            set { lastName = value; }
        }

        protected override object GetIdValue()
        {
            return firstName;
        }
    }


}

I updated my XAML also, with more tests. (It also happened with a ListBox, simply by removing the item from the collection, via code in button click...not event handlers. So, I know it's not a ListView-specific thing.)

<Window x:Class="ListViewDeleteBug.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 xmlns:l="clr-namespace:ListViewDeleteBug"
    Title="ListViewDeleteBug" MinHeight="300" MinWidth="300"
    >
 <StackPanel Orientation="Vertical">
 <Grid>  
  <ListView x:Name="myNames" ItemsSource="{Binding Path=Persons,Mode=OneWay}" KeyDown="OnNamesKeyDown" SelectionMode="Single" IsSynchronizedWithCurrentItem="True"
      >
   <ListView.View>
    <GridView>

     <GridViewColumn Header="Name" DisplayMemberBinding="{Binding FirstName}" Width="300"/>

    </GridView>
   </ListView.View>
  </ListView>

  
 </Grid>
  <!--<ListBox ItemsSource="{Binding Path=Persons,Mode=OneWay}" DisplayMemberPath="FirstName"></ListBox>-->
  <Button Click="OnResetClick">Reset</Button>
  <Button Click="OnRemoveFirstClick">Remove first Name</Button>
  <Button Click="OnRemoveLastClick">Remove last Name</Button>
  <Button Click="OnRemoveNextToLastClick">Remove next to lastlast Name</Button>
 </StackPanel>
</Window>

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;

namespace ListViewDeleteBug
{
    public partial class Window1 : System.Windows.Window
    {
        Roster _roster;

        public Window1()
        {
            InitializeComponent();

            _roster = new Roster();
            DataContext = _roster;
        }

        private void OnNamesKeyDown(object sender, KeyEventArgs args)
        {
            if (myNames.SelectedItem != null)
            {
                Person person = myNames.SelectedItem as Person;
                _roster.Persons.Remove(person);
            }

            args.Handled = true;
        }

        private void OnRemoveFirstClick(object sender, RoutedEventArgs args)
        {
            //_roster.Persons.RemoveAt(0);
            _roster.Persons.removeAtIndex(0);
        }

        private void OnRemoveLastClick(object sender, RoutedEventArgs args)
        {
            //_roster.Persons.RemoveAt(_roster.Persons.Count - 1);
            _roster.Persons.removeAtIndex(_roster.Persons.Count - 1);
        }

        private void OnRemoveNextToLastClick(object sender, RoutedEventArgs args)
        {
            //_roster.Persons.RemoveAt(_roster.Persons.Count - 2);
            _roster.Persons.removeAtIndex(_roster.Persons.Count - 2);
        }

        private void OnResetClick(object sender, RoutedEventArgs args)
        {
            _roster = new Roster();
            DataContext = _roster;
        }
    }
}

RockfordLhotka replied on Friday, July 20, 2007

OK, shouldn't have been so quick...

It appears to be a problem with BLB. Specifically, BLB implements both IBindingList and INotifyCollectionChanged. Somehow in my testing I missed the fact that this is bad.

If you watch closely, you'll find that the correct number of items (one) are being removed from the list, but the UI is somehow missing one of them. That's because data binding is lazy and infers how to do the display update. When it gets the deleted notification from IBindingList it updates. Then it updates again when notified from INotifyCollectionChanged.

How this got missed I'm not sure, but it is a serious issue Sad [:(]

I think 3.0.1 will be coming out sooner than later so I can address this...

In the short term, you can go into BLB and remove all code dealing with INotifyCollectionChanged and the CollectionChanged event, and that appears to solve the issue.

wleary replied on Friday, July 20, 2007

Thank you very much! I commented out the following in BLB, and that seemed to fix it, as you indicated:

protected void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        //if (_nonSerializableCollectionChangedHandlers != null)
        //    _nonSerializableCollectionChangedHandlers.Invoke(this, e);
        //if (_serializableCollectionChangedHandlers != null)
        //    _serializableCollectionChangedHandlers.Invoke(this, e);
    }

Thanks again. Looking forward to the update.

RockfordLhotka replied on Monday, July 23, 2007

I've posted 3.0.1 for download, primarily because it includes the fix for this issue. BLB no longer implements INotifyCollectionChanged, thus avoiding the duplicate eventing issue.

wleary replied on Monday, July 23, 2007

Thanks for the quick responses and the fix Rocky. I have updated our project to use 3.0.1. Thanks!

Copyright (c) Marimer LLC