This article explains how to extend Self-Tracking Entities (STE) from Entity Framework (EF) 4.0 with validation logic and (tracking) state change notification, with just minimal impact on the T4 files. We'll build a two-tier application that submits local changes in a WPF application via a WCF service to a database table. The STE are extended with validation logic that is reusable on client and server. The client is notified when the change tracker of an entity changes its state. The tracking state is displayed to the end user as an icon. Here's the client application in action:
For more details on the foundations of building N-Tier apps with EF 4.0, please read Peter Himschoots article.
Source Code
For the fans of the source-code-first approach, here it is: U2UConsult.SelfTrackingEntities.Sample.zip (622,23 kb)
The structure of the solution is as follows:
Preparation
Database table
First you need a SQL Server table. The provided source code contains a script to generate a working copy of the SalesReason table in the AdventureWorks2008 sample database. This is its initial content:
Data Access Layer
When you have it, it's time to fire up Visual Studio.NET. Create a WCF web service project with an ADO.NET Entity Model. Add the SalesReason2 table to the model (I renamed the entity and entity set to SalesReason and SalesReasons respectively). While you're in the designer, generate the code for the ObjectContext and the Self-Tracking Entities (right click in the designer, select "Add Code Generation Item", select "ADO.NET Self-Tracking Entity Generator"). Add the canonical service methods to fetch the full list of SalesReasons, and to add, delete, and update an individual SalesReason. Here's an example (I personally like to combine Add and Update operations in a Save method):
public List<SalesReason> GetSalesReasons()
{
using (AdventureWorks2008Entities model = new AdventureWorks2008Entities())
{
List<SalesReason> result = new List<SalesReason>();
result.AddRange(model.SalesReasons);
return result;
}
}
public void DeleteSalesReason(SalesReason reason)
{
using (AdventureWorks2008Entities model = new AdventureWorks2008Entities())
{
model.SalesReasons.Attach(reason);
model.SalesReasons.DeleteObject(reason);
model.SaveChanges();
}
}
public SalesReason SaveSalesReason(SalesReason reason)
{
using (AdventureWorks2008Entities model = new AdventureWorks2008Entities())
{
reason.ModifiedDate = DateTime.Now;
if (reason.ChangeTracker.State == ObjectState.Added)
{
model.SalesReasons.AddObject(reason);
model.SaveChanges();
reason.AcceptChanges();
return reason;
}
else if (reason.ChangeTracker.State == ObjectState.Modified)
{
model.SalesReasons.ApplyChanges(reason);
model.SaveChanges();
return reason;
}
else
{
return null; // or an exception
}
}
}
Self Tracking Entities
Add a new class library to the project, call it STE. Drag the Model.tt T4 template from the DAL to the STE project. Add a reference to serialization in the STE project. Add a reference to the STE in the DAL project. Everything should compile again now.
WPF Client
Add a WPF application to the solution. In this client project, add a reference to the STE, and a service reference to the DAL. Add a ListBox and some buttons, with straightforward code behind:
private void RefreshSalesReasons()
{
this.salesReasons = this.GetSalesReasons();
this.SalesReasonsListBox.ItemsSource = this.salesReasons;
}
private ObservableCollection<SalesReason> GetSalesReasons()
{
using (DAL.SalesReasonServiceClient client = new DAL.SalesReasonServiceClient())
{
ObservableCollection<SalesReason> result = new ObservableCollection<SalesReason>();
foreach (var item in client.GetSalesReasons())
{
result.Add(item);
}
return result;
}
}
private void Update_Click(object sender, RoutedEventArgs e)
{
SalesReason reason = this.SalesReasonsListBox.SelectedItem as SalesReason;
if (reason != null)
{
reason.Name += " (updated)";
}
}
private void Insert_Click(object sender, RoutedEventArgs e)
{
SalesReason reason = new SalesReason()
{
Name = "Inserted Reason",
ReasonType = "Promotion"
};
reason.MarkAsAdded();
this.salesReasons.Add(reason);
this.SalesReasonsListBox.ScrollIntoView(reason);
}
private void Delete_Click(object sender, RoutedEventArgs e)
{
SalesReason reason = this.SalesReasonsListBox.SelectedItem as SalesReason;
if (reason != null)
{
reason.MarkAsDeleted();
}
}
private void Commit_Click(object sender, RoutedEventArgs e)
{
using (DAL.SalesReasonServiceClient client = new DAL.SalesReasonServiceClient())
{
foreach (var item in this.salesReasons)
{
switch (item.ChangeTracker.State)
{
case ObjectState.Unchanged:
break;
case ObjectState.Added:
client.SaveSalesReason(item);
break;
case ObjectState.Modified:
client.SaveSalesReason(item);
break;
case ObjectState.Deleted:
client.DeleteSalesReason(item);
break;
default:
break;
}
}
this.RefreshSalesReasons();
}
}
Now you're ready to extend the STE with some extra functionality.
Validation
It's nice to have some business rules that may be checked on the client (to provide immediate feedback to the user) as well as on the server (to prevent corrupt data in the database). This can be accomplished by letting the self-tracking entities implement the IDataErrorInfo interface. This interface just contains an indexer (this[]) to validate an individual property, and an Error property that returns the validation state of the whole instance. Letting the STE implement this interface can be easily done by adding a partial class file. The following example lets the entity complain if its name gets shorter than 5 characters:
public partial class SalesReason : IDataErrorInfo
{
public string Error
{
get
{
return this["Name"];
}
}
public string this[string columnName]
{
get
{
if (columnName == "Name")
{
if (string.IsNullOrWhiteSpace(this.Name) || this.Name.Length < 5)
{
return "Name should have at least 5 characters.";
}
}
return string.Empty;
}
}
}
If you add a data template to the XAML with ValidatesOnDataErrors=true in the binding, then the GUI will respond immediately if a business rule is broken.
XAML:
<TextBox
Width="180"
Margin="0 0 10 0">
<Binding
Path="Name"
Mode="TwoWay"
UpdateSourceTrigger="PropertyChanged"
NotifyOnSourceUpdated="True"
NotifyOnTargetUpdated="True"
ValidatesOnDataErrors="True"
ValidatesOnExceptions="True"/>
</TextBox>
Result:
The same rule can also be checked on the server side, to prevent persisting invalid data in the underlying table:
public SalesReason SaveSalesReason(SalesReason reason)
{
if (!string.IsNullOrEmpty(reason.Error))
{
return null; // or an exception
}
...
Notification of Tracking State Change
By default, an STE's tracking state can be fetched by instance.ChangeTracker.State. This is NOT a dependency property, and its setter doesn't call PropertyChanged. Clients can hook an event handler to the ObjectStateChanging event that is raised just before the state changes (there is no ObjectStateChanged event out of the box). You're free to register even handlers in your client, but then you need to continuously keep track of which change tracker belongs to which entity: assignment, lazy loading, and (de)serialization will make this a cumbersome and error prone endeavour.
To me, it seems more logical that an entity would expose its state as a direct property, with change notification through INotifyPropertyChanged. This can be achieved -again- by adding a partial class file:
public partial class SalesReason
{
private string trackingState;
[DataMember]
public string TrackingState
{
get
{
return this.trackingState;
}
set
{
if (this.trackingState != value)
{
this.trackingState = value;
this.OnTrackingStateChanged();
}
}
}
partial void SetTrackingState(string newTrackingState)
{
this.TrackingState = newTrackingState;
}
protected virtual void OnTrackingStateChanged()
{
if (_propertyChanged != null)
{
_propertyChanged(this, new PropertyChangedEventArgs("TrackingState"));
}
}
}
The only thing you need to do now, is to make sure that the SetTrackingState method is called at the right moment. The end of the HandleObjectStateChanging looks like a nice candidate. Unfortunately this requires a modification of the code that was generated by the T4 template. For performance reasons I used a partial method for this. This is the extract from the SalesReason.cs file:
// This is a new definition
partial void SetTrackingState(string trackingState);
//
private void HandleObjectStateChanging(object sender, ObjectStateChangingEventArgs e)
{
//
this.SetTrackingState(e.NewState.ToString()); // This is a new line
//
if (e.NewState == ObjectState.Deleted)
{
ClearNavigationProperties();
}
}
Just modifiying the generated code is probably not good enough: if later on you need to update your STE (e.g. after adding a column to the underlying table), the modifications will get overriden again. So you might want to modify the source code of the T4 template (search for the HandleObjectStateChanding method and adapt the source code). Fortunately this is a no-brainer: most of the T4 template is just like a C# source code file, but without IntelliSense. The rest of the file looks more like classic ASP - ugh.
Anyway, you end up with a TrackingStatus property to which you can bind user interface elements, wiith or without a converter in between. In the sample application I bound an image to the tracking state:
<Image
Source="{Binding
Path=TrackingState,
Converter={StaticResource ImageConverter}}"
Width="24" Height="24" />
Here's how it looks like:
In general, I think there are not enough partial methods defined and called in the Entity Framework T4 templates. To be frank: 'not enough' is an understatement: I didn't find a single partial method.