Using a CollectionViewSource to display Master-Detail sorted EntityCollections

My goal was simple!  All I wanted to do was take an EntityCollection, sort it, bind it to a ListBox within my WPF window and keep it sorted.  Sounds like it would be a simple task but I now realize that it was much more difficult than it needed to be.  I am pleased to announce that I have a solution to this delemna.  I would be happy to hear your thoughts on this.

Scenario

Consider this simple entity model:

In a WPF window, I would like to see two listboxes.  The first will contain a sorted list of "Roles" and the second will contain a sorted list of the "Users" that belong to the "Role" that is selected in the first listbox.  Here is an example of the window.




What's the Problem?


If you've tried to implement this, you will probably agree that this is not a simple as it looks!

When you set the Source of a CollectionVewSource to an EntityCollection, the type of View that is created is called a BindingListCollectionView.  Unfortunately, this view is not capable of performing sorts.  Well, this isn't entirely true.  It is more accurate to say that the sorting is handled by the underlying collection. In our case, the EntityCollection implements IBindingList and therefore it doesn't support sorting, so, neither will the BindingListCollectionView.  If only the EF guys implemented INotifyCollectionChanged or IBindingListView, things would have been a bit simpler.

My first idea to solving this was to create my own version of the BindingListCollectionView. I was obviously not up to the challenge of rewriting this class as it does quite a bit of stuff so instead I would inherit from it and add whatever logic I needed to make my sort work.  Turns out this isn't possible because the BindingListCollectionVIew is marked as NotInhertiable!

I wasn't too happy at this point and I played with quite a few ideas until I finally gave up.  On my drive home the solution came to me.  If you have ever used an ObservableCollection as the source, you will notice that sorting will in fact work.  So, why not convert the EntityCollection to an ObservableCollection?  Of course, this will need to be done automatically.

After closer inspection, I noticed that whenever I bind the ObservableCollection to a CollectionViewSource, the ListCollectionView is used as the underlying CollectionView structure.  Lucky for me, this is not marked as NotInheritable, so I will use it as the base for my own CollectionView.  If I can get this to work, then you would be able to call up the standard CollectionViewSource in XAML and you wouldn't require any additional code (other than the class itself).


Constructing the SortedEntityCollectionView

As I mentioned above, the class should inherit from ListCollectionView.  We will add our own constructor that will take an IBindingList.  Why IBindingList?  Because, as I mentioned above, the EntityCollection we will receive will implement IBindingList and it is much easier to use since EntityCollection is a generic definition for which I do not want to specify the type argument to.

In our constructor, we need to take the IBindingList (which is really the EntityCollection) and convert it to an ObservableCollection.  I will make a couple of shared (static) helper methods to do this for me.

The first method GetObservableCollection will construct the ObservableCollection.  It will then call the second method ResetObservableCollection to populate the entities contained within the orginal EntityCollection into the new ObservableCollection.  If your not comfortable with Reflection, then you may not entirely understand what is happening, but in short, we need to detect the type of EntityCollection we were supplied and create a new ObservableCollection of the same type.  Because this is all done at runtime, we must continue the trend of using Reflection whenever we call the Add method on the ObservableCollection because our compiler hasn't a clue of what the data type will be.


Public Class SortedEntityCollectionView
Inherits ListCollectionView

    Public Sub New(ByVal source As System.ComponentModel.IBindingList)

        ' Create the ListCollectionView base class.
        ' Instead of passing the source argument received, we will convert it to 
        ' an ObservableCollection.
        MyBase.New(SortedEntityCollectionView.GetObservableCollection(source))

    End Sub

    Private Shared Function GetObservableCollection(ByVal source As System.ComponentModel.IBindingList) As System.Collections.IEnumerable

        ' Use reflection to determine the data type of the generic collection.
        Dim sourceTypes() As System.Type = source.GetType.GetGenericArguments

        ' If this is not a generic collection or if there are two or more generic arguments,
        ' throw an exception.
        If sourceTypes Is Nothing OrElse sourceTypes.Length <> 1 Then
            Throw New ArgumentOutOfRangeException("collection", "Must contain exactly one generic argument.")
        End If

        ' We need to build a valid System.Type of an ObservableCollection with the proper generic argument.
        Dim obsCollectionType As System.Type = GetType(System.Collections.ObjectModel.ObservableCollection(Of )).MakeGenericType(New System.Type() {sourceTypes(0)})

        ' We use the datatype to build our ObservableCollection.
        Dim obsCollection As Object = Activator.CreateInstance(obsCollectionType)

        ' Populate the ObservableCollection with the items that are contained within the source collection.
        SortedEntityCollectionView.ResetObservableCollection(obsCollection, source)

        ' Return the ObservableCollection.
        Return DirectCast(obsCollection, System.Collections.IEnumerable)

    End
Function

    Private Shared Sub ResetObservableCollection(ByVal observableCollection As Object, ByVal sourceCollection As System.ComponentModel.IBindingList)

        ' First remove all of the items in the ObservableCollection.
        observableCollection.GetType.GetMethod("Clear").Invoke(observableCollection, Nothing)

        ' Get the method information for the "Add" method of the ObservableCollection.
        Dim obsAddMethod As System.Reflection.MethodInfo = observableCollection.GetType.GetMethod("Add")

        ' Add each item of the IBindingList to the ObservableCollection.
        For Each item As Object In sourceCollection
            obsAddMethod.Invoke(observableCollection,
New Object() {item})
        Next

    End
Sub

End Class



At this point we were successful at creating our CollectionView and if you were eager to test, you will see that you can in fact sort the collection of Entities.  You will also notice that any changes you make to the EntityCollection (such as additions or removals) are not made in the View.  To keep things syncronized, we need to listen to the events that the EntityCollection is raising and pass them on to the ObservableCollection.

To do this, we will modify our constructor slightly.  We will track the original collection passed to our object.  We will then create a new event handler to listen to the ListChanged event.  (Again, you will recall that the EntityCollection implements IBindingList and hence it must raise the ListChanged event).

Here is the revised constructor and the new event handler.  I also overroad the SourceCollection property to return the original source collection (not our intermediate ObservableCollection).

Edit: I removed the override as I should never have added it. The override causes us to ignore the observable collection, the mechanism used to create a sortable collection.


Public Class SortedEntityCollectionView
    Inherits ListCollectionView

    ' The underlying unfiltered collection.
    Private
WithEvents sourceBindingList As System.ComponentModel.IBindingList


    Public Sub New(ByVal source As System.ComponentModel.IBindingList)

        ' Create the ListCollectionView base class.
        ' Instead of passing the source argument received, we will convert it to 
        ' an ObservableCollection.
        MyBase.New(SortedEntityCollectionView.GetObservableCollection(source))

        ' Keep a reference to the original collection.
        sourceBindingList = source

    End
Sub

    Private Sub SourceBindingList_ListChanged(ByVal sender As Object, ByVal e As System.ComponentModel.ListChangedEventArgs) Handles sourceBindingList.ListChanged

        ' Get a reference to the ObservableCollection that we are using
        ' as a source collection.
        Dim observableCollection As Object = MyBase.SourceCollection

        ' Apply the correct transformation to the underlying ObservableCollection.
        Select Case e.ListChangedType
            Case ComponentModel.ListChangedType.ItemAdded
                observableCollection.GetType.GetMethod(
"Add").Invoke(observableCollection, New Object() {sourceBindingList.Item(e.NewIndex)})
            Case ComponentModel.ListChangedType.ItemDeleted
                observableCollection.GetType.GetMethod(
"RemoveAt").Invoke(observableCollection, New Object() {e.NewIndex})
            Case ComponentModel.ListChangedType.ItemMoved
                observableCollection.GetType.GetMethod(
"Move").Invoke(observableCollection, New Object() {e.OldIndex, e.NewIndex})
            Case ComponentModel.ListChangedType.Reset
                SortedEntityCollectionView.ResetObservableCollection(observableCollection, sourceBindingList)
        End Select

    End
Sub

    Public Overrides ReadOnly Property SourceCollection() As System.Collections.IEnumerable
        Get
            Return
sourceBindingList
        End
Get
    End
Property

End Class



We have constructed the SortedEntityCollectionView and you are ready to use it.  All that is required is that you set the CollectionViewType property of the CollectionViewSource to be the SortedEntityCollectionView type.


Binding the EntityCollection to the Window in WPF

I will now show you how to use the new SortedEntityCollectionView to bind the child EntityCollection (in our case the Users collection) to the second ListBox.

Before I do this, I guess I should quickly cover binding the parent list of entities to the first ListBox.

I assume you are selecting some entities from the database.  You could have something similar to this:

Dim roles as List(Of Role) = ctx.Roles.ToList

Before you bind the list, I suggest that you wrap it in an ObservableCollection.  This way, when you add/remove entities from the list, the changes are automatically sent to the View and its listeners.  This is much cleaner and would essentially mean no extra coding to keep bindings synced.  A disadvantage is that if you want to create a new entity and add it to your context, you will have to also add it to the ObservableCollection so that your GUI will show the new entity.  So, make sure you keep a reference to the ObservableCollection handy.  Also make sure you remember to add the new entity to the ObservableCollection if you want it displayed.

Creating the ObservableCollection is a piece of cake, it is just one line of code:

Dim colRoles As New System.Collections.ObjectModel.ObservableCollection(Of Role) (roles)

Now let's create the CollectionViewSource that is used to sort the collection of roles. I prefer to do this in XAML but it can be done in code if you prefer.  Here is a code snippet.  I am adding it as a resource to my window.

<Window ...
    
xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase" ...

    <
Window.Resources>
        <CollectionViewSource x:Key="CvsRoles">
            <CollectionViewSource.SortDescriptions>
                <scm:SortDescription PropertyName="Name" Direction="Ascending" />
            </CollectionViewSource.SortDescriptions>
        </CollectionViewSource>
    </Window.Resources>
...
</Window>

Notice that I need to declare a namespace so that I can add my SortDescriptions through XAML.  I don't want to go into too much detail here as there are tons of resources on binding with CollectionViewSource objects.

Now that the CollectionViewSource is created, I can set the source of the collection to my ObservableCollection of entities:

DirectCast(Me.Resources("CvsRoles"), CollectionViewSource).Source = colRoles

Then to bind my listbox, I can set the ItemsSource to the CollectionViewSource resource.

<ListBox IsSynchronizedWithCurrentItem="True" ItemsSource="{Binding Source={StaticResource CvsRoles}}" DisplayMemberPath="Name" Name="lstRoles" />

That finishes up the Role ListBox.  Now lets review the Users ListBox (which is the child of a Role).

We will create another CollectionViewSouce for the sub-list however this time we will bind the Source property to the first CollectionViewSource.  This will force the second CollectionViewSource to update when the position of the first is changed.  Another difference is that we need to specify the CollectionViewType.  Don't forget to set your namespace as well.  We get:

<Window ...
    
xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase" ...
    xmlns:my="clr-namespace:WpfApplication"
    ...
    <Window.Resources>

        <
CollectionViewSource x:Key="CvsUsers" Source="{Binding Source={StaticResource CvsRoles}, Path=Users}" CollectionViewType="{x:Type my:SortedEntityCollectionView}">
            <CollectionViewSource.SortDescriptions>
                <scm:SortDescription PropertyName="FirstName" Direction="Ascending" />
            </CollectionViewSource.SortDescriptions>
        </CollectionViewSource>

    </Window.Resources>
...
</Window>



Now, to bind the second listbox we have:

<ListBox IsSynchronizedWithCurrentItem="True" ItemsSource="{Binding Source={StaticResource CvsUsers}}" DisplayMemberPath="FirstName" Name="lstUsers" />


Conclusion

Well, it was a long journey just to sort two listboxes!  Unforunately this is a critical requirement for most developers and it seems Microsoft hasn't really given us a good way to do this.  I hope this will help.

I am sure you're eager to implement this but I need to mention an important limitation I have discovered.  It is not really to do with the class I designed but rather a 'bug' if you will with WPF.  It seems that controls bound to a sorted CollectionViewSource will not stay sorted.  When you add items, they will insert into the proper location however if you change the property of an entity (that would change the sort order) the change will not be made.  Weird stuff happens but in the end the ListBox does not stay sorted.

I have come up with a solution for this and will post another blog entry shortly with my findings and a good workaround.


Source Code

Full Source Code

 

What did you think of this article?




Trackbacks
  • No trackbacks exist for this post.
Comments

  • February 4, 2009 9:52 AM Adam wrote:
    Nicholas

    Fantastic article - just the info I needed, but did you ever find a resolution to keeping the collection sorted?!
    Reply to this
  • February 4, 2009 9:52 PM Nicholas wrote:
    Hi There, I did resolve keeping the collection sorted but have been bad on keeping my promise of posting it.  I will get the article up in the next couple of days.  Thanks for your interest.
    Reply to this
  • April 13, 2010 2:41 PM Randall wrote:
    Nicholas, thanks for the great post. One question: can you set the CollectionViewType property in code rather than XAML and, if so, how?
    Reply to this
  • April 19, 2010 6:46 PM Nicholas wrote:

    Randall

    ,

    I don't have a repro right now, but try this:


    Dim
     cvs As New AutoRefreshCollectionViewSource
    cvs.CollectionViewType =
    GetType (SortedEntityCollectionView)

    After this, set your source.

     

     

     

     

     


    Reply to this
  • October 11, 2010 5:47 PM Gustavo wrote:
    Nicholas, thanks for this article!
    Do you know whether EF4 offers a different work around?

    Thanks!
    Reply to this
  • April 6, 2011 5:19 AM Richard wrote:
    You are my savior I was on the verge of giving up before finding your solution.
    My option was going to be to pull the child entities separately and apply a filter to the collection based on my parent list.selecteditem on every selectedindexchanged event. Ugly but I was out of ideas.

    I'm loving using wpf and ef but am I the only one that thinks the inability to sort the child entities is a fundamental flaw? Forget giving examples of where this facility would be useful, I can't think of many scenarios where you wouldn't want to do this.

    Anyway, sleep well tonight another life saved...
    Reply to this
  • February 1, 2012 5:38 AM rupex wrote:
    The answer to your problem was much simpler than you thought.

    Public ReadOnly Property MyEntity As ListCollectionView
    Get
    Return CollectionViewSource.GetDefaultView(DataContext.MyEntity.ToList)
    End Get
    End Property

    I have this property in a class and I bind that class to the DataGrid. Sorting works no problem.

    I hope this helps
    Reply to this
  • September 27, 2012 8:15 PM Emmanuel Romulus wrote:
    I would like you guys to notice one thing in the classes that entityframe work creates. They are all partial. That means, in you repository you can create partial classes with similar name to extend those classes depending on your need. I needed to do that to safely implement IDataErrforInfo to those classes. That made my life so much easier and simpler. You can additional properties of any type to compensate for your sorting or whatever. Make sure that your partial classes are in the same name space as your entities namespace. The advantage of extending classes through partial, especially in the case of implementing IDataErrorInfo, if you have to update your entity model from database, your implementation will not be lost. I just wanted to share that with you guys. Good day
    Reply to this
  • December 19, 2012 8:40 AM Cedos14 wrote:
    I found a very simple solution that works with EF, you just create a partial class that extends your EF class and then you Add a new Property that return an ObservableCollection from the underlying EntityCollection like this :

    Imports System.Data.Objects.DataClasses

    Partial Public Class Account




    Public ReadOnly Property OperationsList() As System.Collections.ObjectModel.ObservableCollection(Of Operation)
    Get
    Return New System.Collections.ObjectModel.ObservableCollection(Of Operation)(CType(Me, IEntityWithRelationships).RelationshipManager.GetRelatedCollection(Of Operation)("MoneyModel.AccountOperation", "Operation"))
    End Get

    End Property
    End Class
    Reply to this
Leave a comment

Submitted comments are subject to moderation before being displayed.

 Name

 Email (will not be published)

 Website

Your comment is 0 characters limited to 3000 characters.