Tuesday, March 9, 2010

Sorting Out EntityReference Binding

Okay, in my last post on the Entity Framework (EF), I posted code for a custom class that inherits from ObservableCollection<T> called "WPFCollection" and I promised to post some work-around code for the fact that the setter method of an EntityObject's EntityReference properties doesn't fire the EntityObject's INotifyPropertyChanged events (i.e., PropertyChanged/PropertyChanging).  This seems like a big hole to me and I was hoping that it would be plugged in .NET 4.0.  Unfortunately, that doesn't seem to be the case so this post will walk through the work-arounds that I have managed to cull from the web along with some of my own modifications.


EntityReferences and INotifyPropertyChanged
EntityReferences are added to EntityObjects by the Object Relational Designer (ORD) to represent the "1" end of an Association.  These Reference properties are not visible in the *.edmx file as they are basically represented by the Association lines (see the picture to the right).  They do show up in code, and in any class diagram (*.cd) that you might create (see the second picture to the right.

For example, the Trips database table might have a DestinationID field that is a foreign key belonging to a record in a Destinations table.  When the ORD creates the Trip EntityObject, it will automatically add a new property called "DestinationReference." DestinationReference inherits from the EntityReference class and is in addition to the Destination property that is a pointer to an actual Destination EntityObject.  The DestinationReference provides functionality that is useful for navigating the object graph (map) and such.  As mentioned above, the ORD does not place code in the setter methods of EntityReferences to fire the INotifyPropertyChanged events.  This is a big bummer for binding since a binding will have no way of knowing that it needs to update itself.

There are two work-arounds for this situation:
  1. Add some code to the EntityObject that will raise the event, 
  2. Create a custom code generator for ORD that will put the proper INotifyPropertyChanged method calls in the EntityReference setter methods.  
Of these two, the only one I currently know how to accomplish is the first.   It is not an ideal solution because it requires some canned code to be added to EVERY EntityObject class in your model with EntityReferences that you intend to bind.  You need to add a constructor method and then a method that will raise the INotifyPropertyChanged events (note: this code is NOT in the *.designer.cs file, but in a separate partial class file, e.g. "Trip.cs"):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Objects.DataClasses;
using System.ComponentModel;

namespace MyEntityNamespace
{
  partial class Trip : EntityObject
  {
    public Trip()
    {
      foreach (IRelatedEnd relatedEnd in ((IEntityWithRelationships)this).RelationshipManager.GetAllRelatedEnds())
      {
        RelatedEnd association = (RelatedEnd)relatedEnd;
        association.AssociationChanged += new CollectionChangeEventHandler(myAssociationChanged);
      }
    }

    ///<summary>
    /// Fires the PropertyChanged Event for any reassigned EntityReference objects
    ///</summary>
    ///<remarks>
    ///EntityReference objects do not have built in PropertyChanging
    ///and PropertyChanged events in the property

    ///setter.  This method can be assigned to
    ///the AssociationChanged event (usually in the
    ///Constructor) and it will fire the PropertyChanging/ed event.
    ///</remarks>
    private void myAssociationChanged(object sender, CollectionChangeEventArgs e)
    {

      //EntityReferences are changed by first removing the old one and then adding the new one
      if (e.Element != null)
      {
        if (e.Action == CollectionChangeAction.Remove)
          OnPropertyChanging(e.Element.GetType().Name);
        else
          OnPropertyChanged(e.Element.GetType().Name);
      }
    }
  }
}

This code takes advantage of the AssociateChanged event that is fired by an EntityReference when it is added or deleted.  When you reassign and EntityReference, the existing association is first removed and then the new one is added.  The AssociationChanged event is not monitored by bound controls, so this code tacks a listener onto the event that fires the appropriate INotifyPropertyChanged event.  The basis for this code came from an MSDN forum post.

Keeping Things Sorted
I tested out the above code in an example from Julia Lerman's Programming Entity Framework.  In this example, Julia has you build a master-detail form using a ListBox of Trip EntityObjects as the "master."  Trips are differentiated by Destination.DestinationName and StartDate.  Placing my Trips in a WPFCollection<Trip> ObservableCollection and using the code above allows the ListBox to be notified when a Trip's Destination changes so it can update the displayed list.

Side note here for anyone following this example in Julia's book: on page 201 Julia has you define two ComboBox elements, but Julia binds these incorrectly.  Take the Destination one for example.  She binds the ComboBox's SelectedValue property to Destination.DestinationID, however this will not produce proper change notification when changing a Trip Object's Destination (DestinationID for Destination doesn't change!).  If we instead bind SelectedItem to the Destination property (along with the code modifications of the above section), we get proper change notification.  Below is the proper XAML for the Destination ComboBox (also notice the use of a DataContext in the Grid element to simplify the Binding statement):


<Grid DataContext="{Binding ElementName=lbxTrips, Path=SelectedItem}">
...
  <ComboBox Name="cbxDestination"  Grid.Row="1" Grid.Column="3"
            DisplayMemberPath="DestinationName"
            SelectedValuePath="DestinationID"
            SelectedItem="{Binding Destination, Mode=TwoWay}"
            SelectionChanged="cbxDestination_SelectionChanged" />

Okay, back to sorting. The regular CollectionViewSource control does the sorting for you, but it doesn't automatically re-sort when the properties you are sorting on are changed.  I picked up the following code for an AutoRefreshCollectionViewSource class from another forum post, and modified it slightly so that it will play nice with WPFCollection<T>.  Just put the following code in a new Class file called "AutoRefreshCollectionViewSource.cs" (see the new tweaks to this class in this post):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Data;
using System.ComponentModel;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Collections;

namespace WPFApp
{
  public class AutoRefreshCollectionViewSource : CollectionViewSource
  {

    protected override void OnSourceChanged(object oldSource, object newSource)
    {
      if (oldSource != null)
      {
        SubscribeSourceEvents(oldSource, true);
      }

      if (newSource != null)
      {
        SubscribeSourceEvents(newSource, false);
      }
      base.OnSourceChanged(oldSource, newSource);
    }


    private void Item_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
      //Refresh not allowed while in a Transaction
      if (!((ListCollectionView)this.View).IsAddingNew)
      {
        bool refresh = false;
        foreach (SortDescription sort in SortDescriptions)
        {
          if (sort.PropertyName == e.PropertyName)
          {
            refresh = true;
            break;
          }
        }

        if (!refresh)
        {
          foreach (GroupDescription group in GroupDescriptions)
          {
            PropertyGroupDescription propertyGroup = group as PropertyGroupDescription;
            if (propertyGroup != null && propertyGroup.PropertyName == e.PropertyName)
            {
              refresh = true;
              break;
            }
          }
        }

        if (refresh)
        {
          View.Refresh();
         
        }
      }

    }

    private void Source_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
      if (e.Action == NotifyCollectionChangedAction.Add)
      {
        SubscribeItemsEvents(e.NewItems, false);
       
      }
      else if (e.Action == NotifyCollectionChangedAction.Remove)
      {
        SubscribeItemsEvents(e.OldItems, true);
      }
      else
      {
        // TODO: Support this
        Debug.Assert(false);
      }
    }
   
    private void SubscribeItemEvents(object item, bool remove)
    {
      INotifyPropertyChanged notify = item as INotifyPropertyChanged;
      if (notify != null)
      {
        if (remove)
        {
          notify.PropertyChanged -= Item_PropertyChanged;
        }
        else
        {
          notify.PropertyChanged += Item_PropertyChanged;
        }
      }
    }

    private void SubscribeItemsEvents(IEnumerable items, bool remove)
    {
      foreach (object item in items)
      {
        SubscribeItemEvents(item, remove);
      }
    }

    private void SubscribeSourceEvents(object source, bool remove)
    {
      INotifyCollectionChanged notify = source as INotifyCollectionChanged;
      if (notify != null)
      {
        if (remove)
        {
          notify.CollectionChanged -= Source_CollectionChanged;
        }
        else
        {
          notify.CollectionChanged += Source_CollectionChanged;
        }
      }
      SubscribeItemsEvents((IEnumerable)source, remove);
    }
  }
}



This new CollectionViewSource class can be added to your Window resources and bound to a ListBox just as you would a normal CollectionViewSource.  For convenience, here is some sample XAML:

<Window x:Class="WPFApp.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
    xmlns:tns="clr-namespace:WPFApp"
    Title="Window1" Height="600" Width="600" Loaded="Window_Loaded">
    <Window.Resources>
      <tns:AutoRefreshCollectionViewSource x:Key="TripSource" >
        <tns:AutoRefreshCollectionViewSource.SortDescriptions>
          <scm:SortDescription PropertyName="Destination" Direction="Ascending"/>
        </tns:AutoRefreshCollectionViewSource.SortDescriptions>
      </tns:AutoRefreshCollectionViewSource>
    </Window.Resources>

This is pretty standard code, but don't forget the two xml namespaces that you need to make this work.  One for the SortDescription element, "scm," (why this is in a different namespace is a mystery!) and the other which simply points to the project where your AutoRefreshCollectionView is (in this case the current project called WPFApp).  This is completed by assigning the ListBox.itemsource in code as follows:

private WPFCollection<Trip> TripData;
private ListCollectionView TripView;
 ...
private void Window_Loaded(object sender, RoutedEventArgs e)
{
  var tripsQuery = context.Trips.Include("Activities");
  this.TripData = new WPFCollection<Trip>(tripsQuery, context);
  this.TripView = this.TripData.GetCollectionView((CollectionViewSource)this.Resources["TripSource"]);
  this.lbxTrips.ItemsSource = this.TripView;
}

This code utilizes the WPFCollection<T> which I showed in my previous post, but with one additional convenience method called GetCollectionView that allows you to more easily set the View.  Below is the C#  for this new method which you can add to the WPFCollection class (see the tweak to this class in this post).


public ListCollectionView GetCollectionView(CollectionViewSource cvs)
{
  cvs.Source = this;
  return (ListCollectionView)cvs.View;
}

Wrap Up
Well, that was pretty much it.  If you're like me you are wondering why the heck do we need all this code just to have a bound ListBox that automatically re-sorts.  There are two possible answers:
  1. I am doing something wrong
  2. Microsoft is doing something wrong
At the moment, my pride and past experience leads me to believe #2.

No comments:

Post a Comment