- Download source code from here
- Please visit this project site for the latest releases and source code.
Contents
In this article, we will cover the auto-generated IClientChangeTracking
interface, and then we will examine how the methods and properties of this interface can be used inside our demo application for client-side change tracking.
The IClientChangeTracking
interface consists of the following members:
- Method
AcceptChanges()
accepts changes for an entity object.
- Method
AcceptObjectGraphChanges()
accepts changes for an entity object and all objects of its object graph.
- Method
RejectChanges()
rejects changes made to an entity object.
- Method
RejectObjectGraphChanges()
rejects changes made to an entity object and all objects of its object graph.
- Property
HasChanges
is read only, and keeps track of whether an entity object has any changes.
- Method
ObjectGraphHasChanges()
returns whether an entity object along with its object graph has any changes.
- Method
EstimateObjectGraphSize()
returns the estimate size of an entity object along with its object graph.
- Method
EstimateObjectGraphChangeSize()
returns the estimate size of an optimized entity object graph with only objects that have changes.
- Method
GetObjectGraphChanges()
returns an optimized entity object graph with only objects that have changes.
The first four methods can be used to accept or rollback any changes made on an entity object. AcceptChanges()
accepts changes made to the object only, while AcceptObjectGraphChanges()
accepts changes made to the object and all objects of its object graph. Methods RejectChanges()
and RejectObjectGraphChanges()
work in a similar fashion. The next two are property HasChanges
and method ObjectGraphHasChanges()
, which return whether an entity object has any changes. The difference is that the former only checks the entity object itself, while the latter checks the entire object graph. Finally, the last three methods are related and often used together. Method GetObjectGraphChanges()
returns a copy of the calling object graph with only objects that have changes, and the other two are helper methods that return the estimate sizes and help to determine whether it makes sense to call GetObjectGraphChanges()
or not.
Before we visit how the IClientChangeTracking
interface is used on the client-side, let us take a look at the server-side logic first.
Most of the server-side business logic resides in class SchoolService
, and methods inside this class can be roughly divided into data retrieval methods and update methods. Data retrieval methods either return a list of entities or a single entity object, while update methods are called to either add/delete/update a single entity. We will discuss the data retrieval methods next.
When implementing data retrieval methods, one area that we should pay special attention is the ones that expand on multiple levels of navigation properties. Take the GetCourses()
method as an example, this method returns a list of Course
objects and expands on two levels of navigation properties "Enrollments.Student
". So, if we implement this method as follows:
public List<Course> GetCourses()
{
using (var context = new SchoolEntities())
{
return context.Courses
.Include("Enrollments.Student")
.ToList();
}
}
We would retrieve the list of Course
objects as the following diagram shows:
The problem with this list of Course
objects is that each Course
object does not belong to its own object graph and entities "CS111
", "CS112
" are connected through Student
objects. This makes any method that deals with object graph useless. For example, if we make a change to entity "CS111
", a call of ObjectGraphHasChanges()
on entity "CS112
" will also return true
because "CS111
" and "CS112
" belongs to the same object graph.
In order to overcome this problem, the method GetCourses()
has to be modified as follows:
public List<Course> GetCourses()
{
using (var context = new SchoolEntities())
{
var courseList = new List<Course>();
foreach (var course in context.Courses)
{
var currentCourse = course;
using (var innerContext = new SchoolEntities())
{
courseList.Add(
innerContext.Courses
.Include("Enrollments.Student")
.Single(n => n.CourseId == currentCourse.CourseId));
}
}
return courseList;
}
}
This modified GetCourses()
method will return a list of Course
objects as the following diagram shows and we can see that entities "CS111
" and "CS112
" belong to two disconnected object graphs. This time, if we make a change to entity "CS111
", a call of ObjectGraphHasChanges()
on "CS111
" will return true
, while the same call on "CS112
" still returns false
. Since each Course
object belongs to a different object graph, we can detect and save changes to one Course
object without affecting others in the list.
The update methods usually handle add/delete/update operations all within a single method for each entity type. Following is the method UpdateCourse()
that saves changes for a single Course
object no matter if the operation is add, delete or update.
public List<object> UpdateCourse(Course item)
{
var returnList = new List<object>();
try
{
using (var context = new SchoolEntities())
{
switch (item.ChangeTracker.State)
{
case ObjectState.Added:
item.ValidateObjectGraph();
context.Courses.ApplyChanges(item);
context.SaveChanges();
break;
case ObjectState.Deleted:
context.Courses.ApplyChanges(item);
context.SaveChanges();
break;
default:
item.ValidateObjectGraph();
context.Courses.ApplyChanges(item);
context.SaveChanges();
break;
}
}
returnList.Add(string.Empty);
returnList.Add(item.CourseId);
}
catch (OptimisticConcurrencyException)
{
var errorMessage = "Course " + item.CourseId +
" was modified by another user. " +
"Refresh that item before reapply your changes.";
returnList.Add(errorMessage);
returnList.Add(item.CourseId);
}
catch (Exception ex)
{
Exception exception = ex;
while (exception.InnerException != null)
{
exception = exception.InnerException;
}
var errorMessage = "Course " + item.CourseId +
" has error: " + exception.Message;
returnList.Add(errorMessage);
returnList.Add(item.CourseId);
}
return returnList;
}
The Course
entity itself keeps track of all the changes made, and also stores the object's state inside the property ChangeTracker.State
. If the State
is Added, we are going to add a new course. If the State
is Deleted, we will delete that course from the database. And if the State
is either Unchanged or Modified, we will do an update operation. Also, for all three cases, we save changes by simply calling context.Courses.ApplyChanges(item)
followed by context.SaveChanges()
.
This concludes our discussion about the server-side logic. Now we are ready to examine how the IClientChangeTracking
interface can be used on the client side.
Let us take the "Student
" screen as an example, and check what are the basic requirements to implement this screen. First, there should be a list that stores all Student
entities retrieved from the server-side. Second, there should be one variable that points to the current Student
object in edit. Then, there should be Boolean properties that keep track of whether there is any changes made. And finally, there should be a set of methods to retrieve, update and rollback student information. All of these are implemented in class SchoolModel
and can be summarized as follows:
- Property
StudentsList
keeps all Student
entities retrieved from the server-side.
- Property
CurrentStudent
keeps track of what is currently in edit.
- Read only property
StudentsListHasChanges
keeps track of whether StudentsList
has changes.
- Read only property
CurrentStudentHasChanges
keeps track of whether CurrentStudent
has changes.
- Method
GetStudentsAsync()
retrieves a list of Student
entities from the server-side.
- Method
SaveStudentChangesAsync(bool allItems = true)
saves all changed entities from StudentsList
when allItems
is true
, and saves changes from CurrentStudent
when allItems
is set to false
.
- Method
RejectStudentChanges(bool allItems = true)
rolls back all changes from StudentsList
when allItems
is true
, and rolls back changes from CurrentStudent
when allItems
is set to false
.
Boolean properties StudentsListHasChanges
and CurrentStudentHasChanges
store whether there are changes to StudentsList
and CurrentStudent
respectively. To update these two properties, we need to call private
methods ReCalculateStudentsListHasChanges()
and ReCalculateCurrentStudentHasChanges()
shown below, and both methods rely on ObjectGraphHasChanges()
from the IClientChangeTracking
interface, which returns whether an entity object along with its object graph has any changes.
public bool StudentsListHasChanges
{
get { return _studentsListHasChanges; }
private set
{
if (_studentsListHasChanges != value)
{
_studentsListHasChanges = value;
OnPropertyChanged("StudentsListHasChanges");
}
}
}
private bool _studentsListHasChanges;
public bool CurrentStudentHasChanges
{
get { return _currentStudentHasChanges; }
private set
{
if (_currentStudentHasChanges != value)
{
_currentStudentHasChanges = value;
OnPropertyChanged("CurrentStudentHasChanges");
}
}
}
private bool _currentStudentHasChanges;
private void ReCalculateStudentsListHasChanges()
{
StudentsListHasChanges = StudentsList != null
&& StudentsList.Any(n => n.ObjectGraphHasChanges());
}
private void ReCalculateCurrentStudentHasChanges()
{
CurrentStudentHasChanges = CurrentStudent != null
&& CurrentStudent.ObjectGraphHasChanges();
}
Both ReCalculateStudentsListHasChanges()
and ReCalculateCurrentStudentHasChanges()
need to be called from any place where a change to StudentsList
and CurrentStudent
could take place, which will be covered next.
StudentsList
subscribes to the CollectionChanged
event and each Student
object inside the list also subscribes to the PropertyChanged
event. Whenever the CollectionChanged
event fires for the StudentsList
, ReCalculateStudentsListHasChanges()
will recalculate whether StudentsList
has changes or not. Secondly, whenever the PropertyChanged
event fires for any Student
object inside the StudentsList
and the changed property equals HasChanges
, ReCalculateStudentsListHasChanges()
is also gets called to recalculate whether StudentsList
has changes or not. Lastly, if StudentsList
itself is set to point to a different list, method ReCalculateStudentsListHasChanges()
is used again to reset property StudentsListHasChanges
.
public ObservableCollection<Student> StudentsList
{
get { return _studentsList; }
set
{
if (!ReferenceEquals(_studentsList, value))
{
if (_studentsList != null)
{
_studentsList.CollectionChanged -= _studentsList_CollectionChanged;
foreach (var student in _studentsList)
{
((INotifyPropertyChanged)student).PropertyChanged -=
EntityModel_PropertyChanged;
}
}
_studentsList = value;
if (_studentsList != null)
{
_studentsList.CollectionChanged += _studentsList_CollectionChanged;
foreach (var student in _studentsList)
{
((INotifyPropertyChanged)student).PropertyChanged +=
EntityModel_PropertyChanged;
}
}
ReCalculateStudentsListHasChanges();
}
}
private ObservableCollection<Student> _studentsList;
private void _studentsList_CollectionChanged
(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach (Student newItem in e.NewItems)
((INotifyPropertyChanged)newItem).PropertyChanged +=
EntityModel_PropertyChanged;
}
if (e.OldItems != null)
{
foreach (Student oldItem in e.OldItems)
((INotifyPropertyChanged)oldItem).PropertyChanged -=
EntityModel_PropertyChanged;
}
ReCalculateStudentsListHasChanges();
}
private void EntityModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName.Equals("HasChanges"))
{
if (sender is Student)
{
ReCalculateStudentsListHasChanges();
ReCalculateCurrentStudentHasChanges();
}
else if (sender is Instructor)
{
ReCalculateInstructorsListHasChanges();
ReCalculateCurrentInstructorHasChanges();
}
else if (sender is Course || sender is Enrollment)
{
ReCalculateCoursesListHasChanges();
ReCalculateCurrentCourseHasChanges();
}
else
{
throw new NotImplementedException();
}
}
}
CurrentStudent
follows a similar pattern. The difference is that it only subscribes to the PropertyChanged
event. Method ReCalculateCurrentStudentHasChanges()
is called whenever PropertyChanged
event fires and the changed property is HasChanges
. Likewise, when CurrentStudent
is assigned to a different Student object, ReCalculateCurrentStudentHasChanges()
will also update CurrentStudentHasChanges
.
public Student CurrentStudent
{
get { return _currentStudent; }
set
{
if (!ReferenceEquals(_currentStudent, value))
{
if (_currentStudent != null)
{
((INotifyPropertyChanged)_currentStudent).PropertyChanged -=
EntityModel_PropertyChanged;
}
_currentStudent = value;
if (_currentStudent != null)
{
((INotifyPropertyChanged)_currentStudent).PropertyChanged +=
EntityModel_PropertyChanged;
}
ReCalculateCurrentStudentHasChanges();
}
}
}
private Student _currentStudent;
So far, we have shown how to define properties StudentsList
and CurrentStudent
along with two accompanying properties StudentsListHasChanges
and CurrentStudentHasChanges
. These four properties make it possible for the "Student
" screen to display the student information fetched from the database. Also, based on the values of StudentsListHasChanges
and CurrentStudentHasChanges
, we can easily determine whether the "Save", "Save All", "Cancel", and "Cancel All" buttons should be enabled or disabled. There is, however, one small drawback with this design: the property setter for StudentsList
could get a bit complicated if the Student
entity type has many navigation properties and each navigation property expands on multiple levels. Because we need to keep track of changes on multiple navigation properties, all of them have to be subscribed to the PropertyChanged
event.
Next, let us move on to discuss the client-side data retrieval methods for populating the StudentsList
and CurrentStudent
properties.
Data retrieval methods of the SchoolModel
Class are asynchronous methods that use the IAsyncResult
design pattern. Method GetStudentsAsync()
shown below is one of them. It starts retrieving student information through a WCF Service call of BeginGetStudents()
with its second parameter as an AsyncCallback
delegate pointing to BeginGetStudentsComplete
. When this WCF Service call completes, the AsyncCallback
delegate will process the results of the retrieval operation in a separate thread. Since we need to trigger event GetStudentsCompleted
on the UI thread, we have to enclose them inside ThreadHelper.BeginInvokeOnUIThread()
as listed below:
public void GetStudentsAsync(string includeOption, string screenName)
{
_proxy.BeginGetStudents(includeOption, BeginGetStudentsComplete, screenName);
_proxy.IncrementCallCount();
}
private void BeginGetStudentsComplete(IAsyncResult result)
{
ThreadHelper.BeginInvokeOnUIThread(
delegate
{
_proxy.DecrementCallCount();
try
{
var students = _proxy.EndGetStudents(result);
if (GetStudentsCompleted != null)
{
GetStudentsCompleted(this, new ResultsArgs<Student>
(students, null, false, result.AsyncState));
}
}
catch (Exception ex)
{
if (GetStudentsCompleted != null &&
(_lastError == null || AllowMultipleErrors))
{
GetStudentsCompleted(this, new ResultsArgs<Student>
(null, ex, true, result.AsyncState));
}
_lastError = ex;
}
});
}
Similarly, update methods are also asynchronous methods. Our example here is the SaveStudentChangesAsync()
method. This call accepts one Boolean parameter allItems
. If allItems
is true
, it goes through all changed items of StudentsList
and calls BeginUpdateStudent()
. Otherwise, it only checks whether CurrentStudent
has changes, and if that is true
, the method calls BeginUpdateStudent()
for CurrentStudent
only.
SaveStudentChangesAsync()
uses several methods of IClientChangeTracking
interface. First, we use ObjectGraphHasChanges()
to find out whether a Student
object has changes to save or not. Next, we use two helper methods, EstimateObjectGraphSize()
and EstimateObjectGraphChangeSize()
, to determine if the object graph change size is less than 70% of the total size. If this is true
, we call GetObjectGraphChanges()
to get an optimized entity object graph with only objects that have changes.
Method GetObjectGraphChanges()
can be quite useful in reducing the total amount of data sent from client to server side. For example, if we have an order screen that retrieves an order along with hundreds of order detail lines as its navigation collection, and if we only change the order's actual ship date without changing any order detail lines. Calling GetObjectGraphChanges()
before saving this order will make sure that we only send the order object without any order detail lines. Thus, overcoming a major shortcoming of using self-tracking entities.
public void SaveStudentChangesAsync(bool allItems = true)
{
if (allItems)
{
if (StudentsList != null && StudentsListHasChanges)
{
foreach (var student in StudentsList.Where(n => n.ObjectGraphHasChanges()))
{
var totalSize = student.EstimateObjectGraphSize();
var changeSize = student.EstimateObjectGraphChangeSize();
var currentStudent = changeSize < (totalSize*0.7)
? (Student) student.GetObjectGraphChanges()
: student;
_actionQueue.Add(
n => _proxy.BeginUpdateStudent(
currentStudent,
BeginUpdateStudentComplete,
currentStudent.PersonId));
}
if (_actionQueue.BeginOneAction()) _proxy.IncrementCallCount();
}
}
else
{
if (CurrentStudent != null && StudentsList != null && CurrentStudentHasChanges)
{
var currentStudent = StudentsList
.FirstOrDefault(n => n.PersonId == CurrentStudent.PersonId);
if (currentStudent != null)
{
var totalSize = currentStudent.EstimateObjectGraphSize();
var changeSize = currentStudent.EstimateObjectGraphChangeSize();
currentStudent = changeSize < (totalSize*0.7)
? (Student) currentStudent.GetObjectGraphChanges()
: currentStudent;
_actionQueue.Add(
n => _proxy.BeginUpdateStudent(
currentStudent,
BeginUpdateStudentComplete,
currentStudent.PersonId));
if (_actionQueue.BeginOneAction()) _proxy.IncrementCallCount();
}
}
}
}
Method BeginUpdateStudentComplete()
is the AsyncCallback
of BeginUpdateStudent()
described above, and this one processes the results of the asynchronous update operation. If the update is successful and no warning message from the server-side, we call AcceptObjectGraphChanges()
, another method defined inside IClientChangeTracking
interface, which accepts changes for the Student
object and all objects of its object graph. After that, the Student
object's HasChanges
property is set back to false
.
private void BeginUpdateStudentComplete(IAsyncResult result)
{
ThreadHelper.BeginInvokeOnUIThread(
delegate
{
try
{
var returnList = _proxy.EndUpdateStudent(result);
var warningMessage = returnList[0] as string;
var updatedStudentId = Convert.ToInt32(returnList[1]);
var studentId = Convert.ToInt32(result.AsyncState);
var student = StudentsList.Single(n => n.PersonId == studentId);
if (student.ChangeTracker.State == ObjectState.Added)
student.PersonId = updatedStudentId;
if (string.IsNullOrEmpty(warningMessage))
{
var isDeleted = student.ChangeTracker.State == ObjectState.Deleted;
student.AcceptObjectGraphChanges();
if (isDeleted) StudentsList.Remove(student);
if (_actionQueue.BeginOneAction() == false)
{
_proxy.DecrementCallCount();
if (SaveStudentChangesCompleted != null &&
_lastError == null && string.IsNullOrEmpty(warningMessage))
{
SaveStudentChangesCompleted(this,
new ResultArgs<string>(string.Empty, null, false, null));
}
}
}
else
{
_actionQueue.Clear();
_proxy.DecrementCallCount();
if (SaveStudentChangesCompleted != null &&
(_lastError == null || AllowMultipleErrors))
{
SaveStudentChangesCompleted(this,
new ResultArgs<string>(warningMessage, null, true, null));
}
}
}
catch (Exception ex)
{
_actionQueue.Clear();
_proxy.DecrementCallCount();
if (SaveStudentChangesCompleted != null &&
(_lastError == null || AllowMultipleErrors))
{
SaveStudentChangesCompleted(this,
new ResultArgs<string>(string.Empty, ex, true, null));
}
_lastError = ex;
}
});
}
The last method is RejectStudentChanges()
. Just like method SaveStudentChangesAsync()
, RejectStudentChanges()
accepts one Boolean parameter allItems
. If allItems
is true
, the method goes through all changed items of StudentsList
and calls RejectObjectGraphChanges()
(another method of IClientChangeTracking
interface). Otherwise, the method only checks whether CurrentStudent
has changes, and if it has, the method calls RejectObjectGraphChanges()
for the CurrentStudent
only.
public void RejectStudentChanges(bool allItems = true)
{
if (allItems)
{
if (StudentsList != null && StudentsListHasChanges)
{
foreach (var student in StudentsList.Where
(n => n.ObjectGraphHasChanges()).ToList())
{
var isAdded = student.ChangeTracker.State == ObjectState.Added;
student.RejectObjectGraphChanges();
if (isAdded) StudentsList.Remove(student);
}
}
}
else
{
if (CurrentStudent != null && StudentsList != null && CurrentStudentHasChanges)
{
var currentStudent = StudentsList
.FirstOrDefault(n => n.PersonId == CurrentStudent.PersonId);
if (currentStudent != null)
{
var isAdded = currentStudent.ChangeTracker.State == ObjectState.Added;
currentStudent.RejectObjectGraphChanges();
if (isAdded) StudentsList.Remove(currentStudent);
}
}
}
}
We have finished discussing how to use the methods and properties of IClientChangeTracking
interface. To summarize, method ObjectGraphHasChanges()
is used in multiple places to check whether an entity has any changes or not. Secondly, method AcceptObjectGraphChanges()
is only used when an update operation completes successfully, while method RejectObjectGraphChanges()
is called inside a rollback operation to revoke any changes made. Lastly, method GetObjectGraphChanges()
can be quite useful in saving the total amount of data sent over the wire.
I hope you find this article useful, and please rate and/or leave feedback below. Thank you!
- 23rd December, 2011 - Initial release
- 26th December, 2011 - Minor updates to article