Results 1 to 6 of 6

Thread: [.NET 2.0+] Custom Typed Collections and Data-binding Support

  1. #1

    Thread Starter
    Super Moderator jmcilhinney's Avatar
    Join Date
    May 2005
    Location
    Sydney, Australia
    Posts
    110,299

    [.NET 2.0+] Custom Typed Collections and Data-binding Support

    VB version here.

    If we want to use a standard collection in .NET 2.0 or later we use a generic List. If we want to create our own strongly-typed collection we used to have to inherit CollectionBase. That was a pain because, in the days before generics, we used to have to define all the type-specific members ourselves. We would inherit members like Clear and RemoveAt from CollectionBase because they didn't depend on the type of the items. Members like Item and Add though, whose return type or parameter type(s) depends on the type of the items, were left up to us. Since the introduction of generics, life has become much easier. To create a strongly typed collection with all the standard functionality we simply inherit the generic Collection class and that's it:
    CSharp Code:
    1. public class ThingCollection : System.Collections.ObjectModel.Collection<Thing>
    2. {
    3. }
    All the standard functionality is inherited from the base class so we don't need to add any members of our own. That said, if we want to provide custom functionality then we can add our own members. The generic Collection class provides methods that you can override to process items as they are added and removed from the collection. One common use for those members is custom validation, e.g.
    CSharp Code:
    1. public class ThingCollection : System.Collections.ObjectModel.Collection<Thing>
    2. {
    3.     protected override void InsertItem(int index, Thing item)
    4.     {
    5.         bool duplicateID = false;
    6.  
    7.         foreach (Thing existingItem in this.Items)
    8.         {
    9.             if (item.ID == existingItem.ID)
    10.             {
    11.                 duplicateID = true;
    12.                 break;
    13.             }
    14.         }
    15.  
    16.         if (duplicateID)
    17.         {
    18.             // Don't add an item with a duplicate ID.
    19.             throw new ArgumentException("An item with the specified ID already exists");
    20.         }
    21.         else
    22.         {
    23.             // Allow the item to be added.
    24.             base.InsertItem(index, item);
    25.         }
    26.     }
    27.  
    28.     protected override void SetItem(int index, Thing item)
    29.     {
    30.         bool duplicateID = false;
    31.  
    32.         for (int existingIndex = 0; existingIndex < this.Count; existingIndex++)
    33.         {
    34.             // Ignore the existing item at the index being set because it is being replaced.
    35.             if (existingIndex != index && item.ID == this.Items[existingIndex].ID)
    36.             {
    37.                 duplicateID = true;
    38.                 break;
    39.             }
    40.         }
    41.  
    42.         if (duplicateID)
    43.         {
    44.             // Don't add an item with a duplicate ID.
    45.             throw new ArgumentException("An item with the specified ID already exists");
    46.         }
    47.         else
    48.         {
    49.             // Allow the item to be added.
    50.             base.SetItem(index, item);
    51.         }
    52.     }
    53. }
    So, that's nice and easy. Now, with regards to data-binding, you can quite easily bind either a generic List or your own strongly-typed collection to controls in your UI and the data they contain will be displayed. That's because they all implement the IList interface, which is all data-binding requires. The problem is, if you intend to make changes to your collection in code, like adding or removing items or editing properties of the items, then you'll be disappointed if you expect to see those changes reflected in the UI.

    That's because the bound control is not implicitly aware of any changes taking place in the data source. It must be explicitly notified of changes in order to know that it should update its display. In order to provide such notification your data source must implement the IBindingList interface, which neither the generic List nor generic Collection class does. The easy way out of this is to bind your data to a BindingSource and then bind that to the control(s). You can then call ResetCurrentItem, ResetItem or RestBindings on the BindingSource to raise its ListChanged event and thereby notify the bound control to update.

    That's all well and good, but what if the changes to the collection are occurring in code that can't see the BindingSource, like a business logic layer? In that case you need your collection to implement IBindingList itself. You could do that from scratch but you don't actually need to. Instead of deriving your collection directly from the generic Collection class, you can instead inherit the generic BindingList class, which itself inherits Collection and adds an IBindingList implementation. Again, you don't have to add any code of your own if you only want the standard functionality:
    CSharp Code:
    1. public class ThingCollection : System.ComponentModel.BindingList<Thing>
    2. {
    3. }
    Now your collection will raise its ListChanged event automatically whenever you add, insert, set or remove an item or clear the list. Any bound controls will automatically update as a result.

    Now, that's fine for making changes to the list but what about if you make changes to items that are already in the list? Bound controls will not update automatically because the BindingList doesn't raise a ListChanged event automatically because it doesn't inherently know when an item changes. This is where you need to do a little bit of work.

    What needs to happen is that your item class needs to raise an event when a property value changes, e.g.
    CSharp Code:
    1. public class Thing
    2. {
    3.     private string _name;
    4.  
    5.     public string Name
    6.     {
    7.         get
    8.         {
    9.             return this._name;
    10.         }
    11.         set
    12.         {
    13.             if (this._name != value)
    14.             {
    15.                 this._name = value;
    16.                 this.OnNameChanged(EventArgs.Empty);
    17.             }
    18.         }
    19.     }
    20.  
    21.     public event EventHandler NameChanged;
    22.  
    23.     protected virtual void OnNameChanged(EventArgs e)
    24.     {
    25.         if (this.NameChanged != null)
    26.         {
    27.             this.NameChanged(this, e);
    28.         }
    29.     }
    30. }
    Your typed collection can now handle that event and raise its own ListChanged event to notify any bound controls of the change:
    CSharp Code:
    1. public class ThingCollection : System.ComponentModel.BindingList<Thing>
    2. {
    3.     private void HandleNameChanged(object sender, EventArgs e)
    4.     {
    5.         this.OnListChanged(new ListChangedEventArgs(ListChangedType.ItemChanged,
    6.                                                     this.Items.IndexOf((Thing)sender)));
    7.     }
    8. }
    Note that you specify ItemChanged as the change type.

    Now, that method isn't going to handle any events on its own. We need to attach it to the NameChanged event of each item as it gets added to the list. We also need to make sure we detach when an item gets removed from the list. For that we need to override methods inherited from the Collection class, much as we did for the validation earlier:
    CSharp Code:
    1. public class ThingCollection : System.ComponentModel.BindingList<Thing>
    2. {
    3.     protected override void InsertItem(int index, Thing item)
    4.     {
    5.         base.InsertItem(index, item);
    6.  
    7.         // Attach the event handler to the item being added.
    8.         item.NameChanged += new EventHandler(HandleNameChanged);
    9.     }
    10.  
    11.     protected override void SetItem(int index, Thing item)
    12.     {
    13.         // Remove the event handler from the item being removed.
    14.         this.Items[index].NameChanged -= new EventHandler(HandleNameChanged);
    15.  
    16.         base.SetItem(index, item);
    17.  
    18.         // Attach the event handler to the item being added.
    19.         item.NameChanged += new EventHandler(HandleNameChanged);
    20.     }
    21.  
    22.     protected override void RemoveItem(int index)
    23.     {
    24.         // Remove the event handler from the item being removed.
    25.         this.Items[index].NameChanged -= new EventHandler(HandleNameChanged);
    26.  
    27.         base.RemoveItem(index);
    28.     }
    29.  
    30.     protected override void ClearItems()
    31.     {
    32.         // Remove the event handler from all existing items.
    33.         foreach (Thing item in this.Items)
    34.         {
    35.             item.NameChanged -= new EventHandler(HandleNameChanged);
    36.         }
    37.  
    38.         base.ClearItems();
    39.     }
    40.  
    41.     private void HandleNameChanged(object sender, EventArgs e)
    42.     {
    43.         this.OnListChanged(new ListChangedEventArgs(ListChangedType.ItemChanged,
    44.                                                     this.Items.IndexOf((Thing)sender)));
    45.     }
    46. }
    Another useful feature of the IBindingList interface is simple sorting support, i.e. sorting by a single column/property. By adding such support to your typed collection you enable, for instance, a user to click a column header in a DataGridView bound to your collection and have the data sorted automatically. I'll look at that in the next instalment. Stay tuned!

  2. #2

    Thread Starter
    Super Moderator jmcilhinney's Avatar
    Join Date
    May 2005
    Location
    Sydney, Australia
    Posts
    110,299

    Sorting Using a Custom BindingList

    I've attached a test project containing the final Person and PersonCollection classes used in this example, but I'll post code snippets along the way to highlight certain points.

    As I mentioned in the previous post, in order to support sorting when bound a collection must implement the IBindingList interface. As I also mentioned, the easiest way to implement the IBindingList interface is to inherit the BindingList class:
    CSharp Code:
    1. public class PersonCollection : System.ComponentModel.BindingList<Person>
    2. {
    3. }
    Now, in order to support sorting in our collection we must do three things:

    1. Override the SupportsSorting property and return True;
    2. Override the ApplySortCore method and implement our sort; and
    3. Override the RemoveSortCore method and remove our sort.
    CSharp Code:
    1. using System.ComponentModel;
    2.  
    3. public class PersonCollection : System.ComponentModel.BindingList<Person>
    4. {
    5.     protected override bool SupportsSortingCore
    6.     {
    7.         get
    8.         {
    9.             return true;
    10.         }
    11.     }
    12.  
    13.     protected override void ApplySortCore(PropertyDescriptor prop, ListSortDirection direction)
    14.     {
    15.         base.ApplySortCore(prop, direction);
    16.     }
    17.  
    18.     protected override void RemoveSortCore()
    19.     {
    20.         base.RemoveSortCore();
    21.     }
    22. }
    Next, if we're going to sort the items in our collection, we need some way to compare them. If the item class is under your control then you can make it implement the IComparable interface and then items can be compared directly. That's not much good if we want to sort by different properties at different times though. In that case we need to define a new class that implements the IComparer interface. It does much the same job as IComparable but from outside the objects instead of from inside.

    Data-binding makes heavy use of PropertyDescriptors. As the name suggests, A PropertyDescriptor is an object that describes a property, which is why only properties can be bound and not fields. As such, our IComparer implementation should also be based on PropertyDescriptors:
    CSharp Code:
    1. private class PersonComparer : System.Collections.Generic.IComparer<Person>
    2. {
    3.     private PropertyDescriptor prop;
    4.     private ListSortDirection direction;
    5.  
    6.     public PersonComparer(PropertyDescriptor prop, ListSortDirection direction)
    7.     {
    8.         this.prop = prop;
    9.         this.direction = direction;
    10.     }
    11.  
    12.     public int Compare(Person x, Person y)
    13.     {
    14.         // Get a value that indicates the relative positions of the values of the specified property.
    15.         int result = ((IComparable)this.prop.GetValue(x)).CompareTo(this.prop.GetValue(y));
    16.  
    17.         // If the sort order is descending...
    18.         if (this.direction == ListSortDirection.Descending)
    19.         {
    20.             // ...reverse the relative positions.
    21.             result = -result;
    22.         }
    23.  
    24.         return result;
    25.     }
    26. }
    Note that this class is declared Private because, in the example, I've declared it inside the PersonCollection class. That's the only place it gets used so it makes sense.

    Now, how does this class work? You create an instance by specifying the property you want to compare by, as a PropertyDescriptor, and a sort direction. Obviously the relative positions of the two objects will be reversed if the sort direction is reversed. You can pass any two Person objects to this instance's Compare method and it will provide a value that indicates their relative order.

    Notice that I cast the first property value as type IComparable, which is done to access its CompareTo method in order to compare it to the other property value. This requires that the type of the property actually does implement IComparable. This will always be the case for primitive types like String and Integer, which are always directly comparable, but will not be the case for more complex types. For instance, you wouldn't sort a collection of DataTables by their Rows properties because it doesn't make sense to compare two DataRowCollections and get a relative order.

    Now, when sorting a list of items, such comparisons are performed repeatedly in order to shuffle pairs of items into the correct order until all items are ordered as desired. Our ApplySortCore method must do just that:
    CSharp Code:
    1. protected override void ApplySortCore(PropertyDescriptor prop, ListSortDirection direction)
    2. {
    3.     Person[] items = new Person[this.Items.Count];
    4.  
    5.     this.Items.CopyTo(items, 0);
    6.     Array.Sort(items,
    7.                new PersonComparer(prop,
    8.                                   direction));
    9.  
    10.     for (int index = 0; index < items.Length; index++)
    11.     {
    12.         this.Items[index] = items[index];
    13.     }
    14.  
    15.     this.OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, 0));
    16. }
    In this method we create an array containing all our items, sort it using an instance of our IComparer, then replace the existing items with this sorted set. They are the same items but in a different order. There may well be more efficient ways to implement the sort but that will do for this example.

    Note that this ApplySortCore method is what gets invoked when your collection is bound to a DataGridView and the user clicks a column header. This may seem a little strange because the method is declared Protected, so it should not be accessible outside the class. In fact, the ApplySortCore method is NOT accessible outside the class. ApplySortCore is an explicit implementation of the IBindingList.ApplySort method though. That means that you cannot call ApplySortCore on a PersonCollection object, but if you cast it as type IBindingList and call ApplySort then you will invoke that method. This is exactly what happens when your collection is bound.

    This all allows you to sort your collection when bound to your UI. It does make sorting a collection in code somewhat cumbersome though. You'd have to create a PropertyDescriptor, cast the collection as type IBindingList and call ApplySort. To avoid this, I've added a Sort property to the PersonCollection class in the attached project. It works much like the Sort property of the BindingSource and DataView classes, but it only supports one property at a time.

    When you run the attached project you'll see a small PersonCollection bound to a DataGridView. Try clicking the column headers and watch the data get sorted. Now try entering a sort clause in the TextBox and clicking the Sort button. You can specify any of the three property names and, optionally, a direction. If the direction is omitted then ascending is assumed. Note that the property name is case sensitive but the direction is not. Examples of valid clauses are "LastName", "FirstName ASC" and "ID DESC".

    One final point to note. This implementation is not perfect. For instance, if you sort the collection and then add a new item it will not take into account the current sort order. This may be desirable but, in case it's not, I'll look at fixing that in the next installment. Stay tuned!
    Attached Files Attached Files

  3. #3

    Thread Starter
    Super Moderator jmcilhinney's Avatar
    Join Date
    May 2005
    Location
    Sydney, Australia
    Posts
    110,299

    Dynamic Sorting

    For this example I used the project from the previous post as a starting point, modified it and I've attached the new version to this post. This new version addresses the issue I mentioned at the end of the previous post. Now, if you sort the grid, either by clicking a column header or the Sort button, and then you edit the data, the grid will resort itself automatically. How is this achieved? Read on.

    The first step was to make the Person class implement the INotifyPropertyChanged interface:
    CSharp Code:
    1. using System.ComponentModel;
    2.  
    3. public class Person : System.ComponentModel.INotifyPropertyChanged
    4. {
    5.     private int _id;
    6.     private string _firstName;
    7.     private string _lastName;
    8.  
    9.  
    10.     public int ID
    11.     {
    12.         get
    13.         {
    14.             return this._id;
    15.         }
    16.         set
    17.         {
    18.             if (this._id != value)
    19.             {
    20.                 this._id = value;
    21.                 this.OnPropertyChanged(new PropertyChangedEventArgs("ID"));
    22.             }
    23.         }
    24.     }
    25.  
    26.     public string FirstName
    27.     {
    28.         get
    29.         {
    30.             return this._firstName;
    31.         }
    32.         set
    33.         {
    34.             if (this._firstName != value)
    35.             {
    36.                 this._firstName = value;
    37.                 this.OnPropertyChanged(new PropertyChangedEventArgs("FirstName"));
    38.             }
    39.         }
    40.     }
    41.  
    42.     public string LastName
    43.     {
    44.         get
    45.         {
    46.             return this._lastName;
    47.         }
    48.         set
    49.         {
    50.             if (this._lastName != value)
    51.             {
    52.                 this._lastName = value;
    53.                 this.OnPropertyChanged(new PropertyChangedEventArgs("LastName"));
    54.             }
    55.         }
    56.     }
    57.  
    58.  
    59.     public Person(int id, string firstName, string lastName)
    60.     {
    61.         this._id = id;
    62.         this._firstName = firstName;
    63.         this._lastName = lastName;
    64.     }
    65.  
    66.  
    67.     /// <summary>
    68.     /// Occurs when a property value changes.
    69.     /// </summary>
    70.     public event PropertyChangedEventHandler PropertyChanged;
    71.  
    72.  
    73.     /// <summary>
    74.     /// Raises the PropertyChanged event.
    75.     /// </summary>
    76.     protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
    77.     {
    78.         if (this.PropertyChanged != null)
    79.         {
    80.             this.PropertyChanged(this, e);
    81.         }
    82.     }
    83. }
    Now a Person object will notify any listeners each time one of its property values changes. This has two effects:

    1. If we edit a property value of a Person object in code, that change will be reflected automatically in any bound controls. That's because bound controls listen for the PropertyChanged event of the items they're bound to.

    2. Our collection knows when a property value of an item changes so it can resort itself if necessary.

    The second effect is what we're specifically interested in here. In order to implement this we must first add an event handler to the PersonCollection class to handle the PropertyChanged event of its items:
    CSharp Code:
    1. /// <summary>
    2. /// Handles the PropertyChanged event for all items in the collection.
    3. /// </summary>
    4. /// <param name="sender">
    5. /// The item whose property value has changed.
    6. /// </param>
    7. /// <param name="e">
    8. /// Contains the name of the property whose value has changed.
    9. /// </param>
    10. private void Person_PropertyChanged(object sender, PropertyChangedEventArgs e)
    11. {
    12.     if (this._sortProperty.Name == e.PropertyName)
    13.     {
    14.         // A value has changed in the property by which the items are sorted so resort the collection.
    15.         this.ApplySortCore(this._sortProperty, this._sortDirection);
    16.     }
    17. }
    Now, this method is supposed to handle the PropertyChanged event for all the items in the collection and none that aren't. This doesn't happen automatically. We need to attach the event handler as an item is added to the collection and, just as importantly, we need to detach the event handler as an item is removed from the collection:
    CSharp Code:
    1. /// <summary>
    2. /// Removes all elements from the collection.
    3. /// </summary>
    4. protected override void ClearItems()
    5. {
    6.     // Detach event handlers from all items.
    7.     foreach (Person item in this.Items)
    8.     {
    9.         item.PropertyChanged -= new PropertyChangedEventHandler(Person_PropertyChanged);
    10.     }
    11.  
    12.     base.ClearItems();
    13. }
    14.  
    15. /// <summary>
    16. /// Inserts the specified item in the list at the specified index.
    17. /// </summary>
    18. protected override void InsertItem(int index, Person item)
    19. {
    20.     base.InsertItem(index, item);
    21.  
    22.     // Attach event handlers to the item being added.
    23.     item.PropertyChanged += new PropertyChangedEventHandler(Person_PropertyChanged);
    24. }
    25.  
    26. /// <summary>
    27. /// Removes the item at the specified index.
    28. /// </summary>
    29. protected override void RemoveItem(int index)
    30. {
    31.     // Detach event handlers from the item being removed.
    32.     this.Items[index].PropertyChanged -= new PropertyChangedEventHandler(Person_PropertyChanged);
    33.  
    34.     base.RemoveItem(index);
    35. }
    36.  
    37. /// <summary>
    38. /// Replaces the item at the specified index with the specified item.
    39. /// </summary>
    40. protected override void SetItem(int index, Person item)
    41. {
    42.     // Detach event handlers from the item being removed.
    43.     this.Items[index].PropertyChanged -= new PropertyChangedEventHandler(Person_PropertyChanged);
    44.  
    45.     base.SetItem(index, item);
    46.  
    47.     // Attach event handlers to the item being added.
    48.     item.PropertyChanged += new PropertyChangedEventHandler(Person_PropertyChanged);
    49. }
    That looks after resorting when existing items are edited. We need only extend that slightly to resort the collection when new items are added:
    CSharp Code:
    1. /// <summary>
    2. /// Inserts the specified item in the list at the specified index.
    3. /// </summary>
    4. protected override void InsertItem(int index, Person item)
    5. {
    6.     base.InsertItem(index, item);
    7.  
    8.     // Attach event handlers to the item being added.
    9.     item.PropertyChanged += new PropertyChangedEventHandler(Person_PropertyChanged);
    10.  
    11.     if (this._sortProperty != null)
    12.     {
    13.         // Ensure that the collection maintains the correct sort order.
    14.         this.ApplySortCore(this._sortProperty, this._sortDirection);
    15.     }
    16. }
    17.  
    18. /// <summary>
    19. /// Replaces the item at the specified index with the specified item.
    20. /// </summary>
    21. protected override void SetItem(int index, Person item)
    22. {
    23.     // Detach event handlers from the item being removed.
    24.     this.Items[index].PropertyChanged -= new PropertyChangedEventHandler(Person_PropertyChanged);
    25.  
    26.     base.SetItem(index, item);
    27.  
    28.     // Attach event handlers to the item being added.
    29.     item.PropertyChanged += new PropertyChangedEventHandler(Person_PropertyChanged);
    30.  
    31.     if (this._sortProperty != null)
    32.     {
    33.         // Ensure that the collection maintains the correct sort order.
    34.         this.ApplySortCore(this._sortProperty, this._sortDirection);
    35.     }
    36. }
    Now, if the collection is currently sorted, it is resorted each time a new item is added.

    To test this out, try running the attached project and click a column header to sort the grid by that column. Now try editing a cell in that column such that it would make the rows out of order. Note that when you navigate away from that cell the grid resorts to maintain correct order in the sorted column. Now try entering new values in the controls at the bottom of the form and hitting the Add button. Make sure that adding the new item at the bottom of the grid would make the rows out of order. Note that the grid resorts itself such that the new item is placed to maintain correct order in the sorted column.

    Again, I should point out that this code has not been implemented in the most efficient way possible. I've used the easiest way to provide sorting but the best way would require some more work. For instance, when an item is added, at the moment it is added to the end of the collection and the grid will update, then the collection is resorted and the grid will update again. In this example you don't see a difference and there are so few items that everything happens quickly enough that it doesn't matter. With a larger number of items it might become apparent that the grid was refreshing twice. Ideally we would avoid updating the grid twice by ensuring that the new item is inserted at the correct index to begin with. I'll look at optimising this code at a later time. Stay tuned!
    Attached Files Attached Files

  4. #4
    Fanatic Member Andy_P's Avatar
    Join Date
    May 2005
    Location
    Dunstable, England
    Posts
    669

    Re: [.NET 2.0+] Custom Typed Collections and Data-binding Support

    Outstanding. Many thanks for this.
    Using Windows XP Home sp3
    Mucking around with C# 2008 Express
    while ( this.deadHorse ) { flog( ); }


  5. #5
    New Member
    Join Date
    Jun 2010
    Posts
    2

    Re: [.NET 2.0+] Custom Typed Collections and Data-binding Support

    First create a class that implements [IComparer] interface and define the [Compare] method.Following is the class that we will use for sorting Clients order by last name.
    [VB.NET CODE STARTS]

    Public Class ClientComparer
    Implements IComparer

    Public Function Compare(ByVal x As Object, ByVal y As Object) As Integer Implements System.Collections.IComparer.Compare
    Dim objClientX As Client = CType(x, Client) ' Client is the class having FirtsName, LastName as properties
    Dim objClientY As Client = CType(y, Client)

    Dim sX As String = UCase(objClientX.LastName) ' comparision is made using the LAST NAME OF CLIENT
    Dim sY As String = UCase(objClientY.LastName)

    If sX = sY Then
    Return 0
    End If

    If sX.Length > sY.Length Then
    sX = sX.Substring(0, sY.Length)
    If sX = sY Then
    Return 1
    End If
    ElseIf sX.Length < sY.Length Then
    sY = sY.Substring(0, sX.Length)
    If sX = sY Then
    Return -1
    End If
    End If

    For i As Integer = 0 To sX.Length
    If Not sX.Substring(i, 1) = sY.Substring(i, 1) Then
    Return Asc(CType(sX.Substring(i, 1), Char)) - Asc(CType(sY.Substring(i, 1), Char))
    End If
    Next
    End Function

    End Class

    [VB.NET CODE ENDS]

    Hope it would answer your question.

  6. #6

    Thread Starter
    Super Moderator jmcilhinney's Avatar
    Join Date
    May 2005
    Location
    Sydney, Australia
    Posts
    110,299

    Re: [.NET 2.0+] Custom Typed Collections and Data-binding Support

    Quote Originally Posted by eliza_81 View Post
    Hope it would answer your question.
    Um, there was no question. This is an example, in C#, of how to create a custom collection that supports data-binding. A VB example of how to sort a collection isn;t really relevant to this thread.

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •  



Click Here to Expand Forum to Full Width