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:
- Add some code to the EntityObject that will raise the event,
- Create a custom code generator for ORD that will put the proper INotifyPropertyChanged method calls in the EntityReference setter methods.
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);
}
}
}
}
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" />
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>
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>
private WPFCollection<Trip> TripData;
private ListCollectionView TripView;
{
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;
}
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:
- I am doing something wrong
- Microsoft is doing something wrong
No comments:
Post a Comment