Building WPF Applications with Self-Tracking Entity Generator and Visual Studio 2012 - IClientChangeTracking Interface





5.00/5 (3 votes)
This article describes the IClientChangeTracking interface generated by Self-Tracking Entity Generator and Visual Studio 2012.
- Download source code from here
- Please visit this project site for the latest releases and source code.
Contents
- Introduction
- IClientChangeTracking Interface
- SchoolService Class (Server-side)
- SchoolModel Class (Client-side)
- StudentsListHasChanges and CurrentStudentHasChanges
- StudentsList and CurrentStudent
- Data Retrieval Methods
- Update Methods
- Rollback Methods
- Wrapping up
- History
Introduction
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. Please note that this article is
based on a previous
article on Self-Tracking Entity Generator for Visual Studio 2010 with only
minor updates. So, if you have read the previous one, you can safely skip the
rest of this article.
IClientChangeTracking Interface
The IClientChangeTracking
interface consists of the following
members:
AcceptChanges()
accepts changes for an entity object.AcceptObjectGraphChanges()
accepts changes for an entity object and all objects of its object graph.RejectChanges()
rejects changes made to an entity object.RejectObjectGraphChanges()
rejects changes made to an entity object and all objects of its object graph.ObjectGraphHasChanges()
returns whether an entity object along with its object graph has any changes.EstimateObjectGraphSize()
returns the estimate size of an entity object along with its object graph.EstimateObjectGraphChangeSize()
returns the estimate size of an optimized entity object graph with only objects that have changes.GetObjectGraphChanges()
returns an optimized entity object graph with only objects that have changes.
HasChanges
is read only, and keeps track of whether an entity object has any 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. The
RejectChanges()
and RejectObjectGraphChanges()
methods
work in a similar fashion. The next two are the property
HasChanges
and the 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. The GetObjectGraphChanges()
method 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.
SchoolService Class (Server-side)
Most of the server-side business logic resides in the
SchoolService
class, 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.
Data Retrieval Methods
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 the 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
"
belong to the same object graph.
In order to overcome this problem, the actual implementation of the
GetCourses()
method is as follows:
public List<Course> GetCourses(ClientQuery clientQuery)
{
using (var context = new SchoolEntities())
{
if (clientQuery.IncludeList.Count == 0)
{
return context.Courses.ApplyClientQuery(clientQuery).ToList();
}
var courseList = new List<Course>();
foreach (var course in context.Courses.ApplyClientQuery(clientQuery).ToList())
{
var currentCourse = course;
using (var innerContext = new SchoolEntities())
{
courseList.Add(
innerContext.Courses
.ApplyIncludePath(clientQuery)
.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.
Update Methods
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>();
if (item == null)
{
returnList.Add("Course cannot be null.");
returnList.Add(0);
return returnList;
}
try
{
using (var context = new SchoolEntities())
{
switch (item.ChangeTracker.State)
{
case ObjectState.Added:
// server side validation
item.ValidateObjectGraph();
// verify whether the instructor for this course exists
bool instructorExists = context.People.OfType<Instructor>()
.Any(n => n.PersonId == item.InstructorId);
if (!instructorExists)
{
returnList.Add("Error_CannotAddCourseNoInstructor");
returnList.Add(item.CourseId);
return returnList;
}
// verify all enrollments for this course have valid students
bool enrollmentsValid = item.Enrollments
.Aggregate(true, (current, enrollment) =>
current && context.People.OfType<Student>()
.Any(n => n.PersonId == enrollment.StudentId));
if (!enrollmentsValid)
{
returnList.Add("Error_CannotAddCourseNoStudent");
returnList.Add(item.CourseId);
return returnList;
}
// save changes
context.Courses.ApplyChanges(item);
context.SaveChanges();
break;
case ObjectState.Deleted:
// save changes
context.Courses.ApplyChanges(item);
context.SaveChanges();
break;
default:
// server side validation
item.ValidateObjectGraph();
// verify whether the instructor for this course exists
instructorExists =
context.People.OfType<Instructor>()
.Any(n => n.PersonId == item.InstructorId);
if (!instructorExists)
{
returnList.Add("Error_CannotUpdateCourseNoInstructor");
returnList.Add(item.CourseId);
return returnList;
}
// verify all enrollments for this course have valid students
enrollmentsValid = item.Enrollments
.Aggregate(true, (current, enrollment) =>
current && context.People.OfType<Student>()
.Any(n => n.PersonId == enrollment.StudentId));
if (!enrollmentsValid)
{
returnList.Add("Error_CannotUpdateCourseNoStudent");
returnList.Add(item.CourseId);
return returnList;
}
// save changes
context.Courses.ApplyChanges(item);
context.SaveChanges();
break;
}
}
returnList.Add(string.Empty);
returnList.Add(item.CourseId);
}
catch (OptimisticConcurrencyException)
{
returnList.Add("Error_CourseModifiedByAnotherUser");
returnList.Add(item.CourseId);
}
catch (Exception ex)
{
Exception exception = ex;
while (exception.InnerException != null)
{
exception = exception.InnerException;
}
var errorMessage = 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.
SchoolModel Class (Client-side)
Let us take the "Student
" screen as an example, and check 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 are 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 the class SchoolModel
and can be summarized as follows:
StudentsList
keeps allStudent
entities retrieved from the server-side.CurrentStudent
keeps track of what is currently in edit.- Read only property
StudentsListHasChanges
keeps track of whetherStudentsList
has changes. - Read only property
CurrentStudentHasChanges
keeps track of whetherCurrentStudent
has changes.
GetStudentsAsync()
retrieves a list ofStudent
entities from the server-side.SaveStudentChangesAsync(bool allItems = true)
saves all changed entities fromStudentsList
whenallItems
is true, and saves changes fromCurrentStudent
whenallItems
is set to false.RejectStudentChanges(bool allItems = true)
rolls back all changes fromStudentsList
whenallItems
is true, and rolls back changes fromCurrentStudent
whenallItems
is set to false.
StudentsListHasChanges and CurrentStudentHasChanges
Boolean properties StudentsListHasChanges
and
CurrentStudentHasChanges
store whether there are changes to
StudentsList
and CurrentStudent
, respectively. To
update these two properties, we need to call the 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()
{
// re-calculate StudentsListHasChanges
StudentsListHasChanges = StudentsList != null
&& StudentsList.Any(n => n.ObjectGraphHasChanges());
}
private void ReCalculateCurrentStudentHasChanges()
{
// re-calculate CurrentStudentHasChanges
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 and CurrentStudent
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()
also
gets called to recalculate whether StudentsList
has changes or
not. Lastly, if StudentsList
itself is set to point to a different
list, the ReCalculateStudentsListHasChanges()
method is used again
to reset the 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. The method
ReCalculateCurrentStudentHasChanges()
is called whenever the
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
Data retrieval methods of the SchoolModel
class are asynchronous
methods that use the IAsyncResult
design pattern. The
GetStudentsAsync()
method 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 the 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();
}
/// <summary>
/// AsyncCallback for BeginGetStudents
/// </summary>
/// <param name="result"></param>
private void BeginGetStudentsComplete(IAsyncResult result)
{
ThreadHelper.BeginInvokeOnUIThread(
delegate
{
_proxy.DecrementCallCount();
try
{
// get the return values
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;
}
});
}
Update Methods
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 the
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.
The GetObjectGraphChanges()
method 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.
/// <summary>
/// If allItems is true, all items from the StudentsList have
/// their changes saved; otherwise, only CurrentStudent from
/// the StudentsList has its changes saved.
/// </summary>
/// <param name="allItems"></param>
public void SaveStudentChangesAsync(bool allItems = true)
{
if (allItems)
{
if (StudentsList != null && StudentsListHasChanges)
{
// save changes for all items from the StudentsList
foreach (var student in StudentsList.Where(n => n.ObjectGraphHasChanges()))
{
var totalSize = student.EstimateObjectGraphSize();
var changeSize = student.EstimateObjectGraphChangeSize();
// if the optimized entity object graph is less than 70%
// of the original, call GetObjectGraphChanges()
var currentStudent = changeSize < (totalSize*0.7)
? (Student) student.GetObjectGraphChanges()
: student;
_actionQueue.Add(
n => _proxy.BeginUpdateStudent(
currentStudent,
BeginUpdateStudentComplete,
currentStudent.PersonId));
}
// start save changes for the first student
if (_actionQueue.BeginOneAction()) _proxy.IncrementCallCount();
}
}
else
{
if (CurrentStudent != null && StudentsList != null && CurrentStudentHasChanges)
{
// save changes for only CurrentStudent from the StudentsList
var currentStudent = StudentsList
.FirstOrDefault(n => n.PersonId == CurrentStudent.PersonId);
if (currentStudent != null)
{
var totalSize = currentStudent.EstimateObjectGraphSize();
var changeSize = currentStudent.EstimateObjectGraphChangeSize();
// if the optimized entity object graph is less than 70%
// of the original, call GetObjectGraphChanges()
currentStudent = changeSize < (totalSize*0.7)
? (Student) currentStudent.GetObjectGraphChanges()
: currentStudent;
_actionQueue.Add(
n => _proxy.BeginUpdateStudent(
currentStudent,
BeginUpdateStudentComplete,
currentStudent.PersonId));
// start save changes for the current student
if (_actionQueue.BeginOneAction()) _proxy.IncrementCallCount();
}
}
}
}
The BeginUpdateStudentComplete()
method is the
AsyncCallback
of BeginUpdateStudent()
described above,
and it processes the results of the asynchronous update operation. If the
update is successful and there is no warning message from the server-side, we
call AcceptObjectGraphChanges()
, another method defined inside the
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.
/// <summary>
/// AsyncCallback for BeginUpdateStudent
/// </summary>
/// <param name="result"></param>
private void BeginUpdateStudentComplete(IAsyncResult result)
{
ThreadHelper.BeginInvokeOnUIThread(
delegate
{
try
{
// get the return values
var returnList = _proxy.EndUpdateStudent(result);
// returnList[0] could be a resource key or warning message
var resourceKey = returnList[0] as string;
// returnList[1] is the updated student Id
var updatedStudentId = Convert.ToInt32(returnList[1]);
// retrieve the actual warning message
var warningMessage = GetWarningMessageFromResource(resourceKey, updatedStudentId);
// get the studentId for the student that finished saving changes
var studentId = Convert.ToInt32(result.AsyncState);
var student = StudentsList.Single(n => n.PersonId == studentId);
// update the student Id if the student State is Added
if (student.ChangeTracker.State == ObjectState.Added)
student.PersonId = updatedStudentId;
if (string.IsNullOrEmpty(warningMessage))
{
var isDeleted = student.ChangeTracker.State == ObjectState.Deleted;
// if there is no error or warning message,
// call AcceptObjectGraphChanges() first
student.AcceptObjectGraphChanges();
// if State is Deleted, remove the student from the StudentsList
if (isDeleted) StudentsList.Remove(student);
// then, continue to save changes for the next student in queue
if (_actionQueue.BeginOneAction() == false)
{
// all changes are saved, we need to send notification
_proxy.DecrementCallCount();
if (SaveStudentChangesCompleted != null &&
_lastError == null && string.IsNullOrEmpty(warningMessage))
{
SaveStudentChangesCompleted(this,
new ResultArgs<string>(string.Empty, null, false, null));
}
}
}
else
{
// if there is a warning message,
// we need to stop and send notification
// on first occurrence, in other words, if _lastError is still null
_actionQueue.Clear();
_proxy.DecrementCallCount();
if (SaveStudentChangesCompleted != null &&
(_lastError == null || AllowMultipleErrors))
{
SaveStudentChangesCompleted(this,
new ResultArgs<string>(warningMessage, null, true, null));
}
}
}
catch (Exception ex)
{
// if there is an error, we need to stop and send notification
// on first occurrence, in other words, if _lastError is still null
_actionQueue.Clear();
_proxy.DecrementCallCount();
if (SaveStudentChangesCompleted != null &&
(_lastError == null || AllowMultipleErrors))
{
SaveStudentChangesCompleted(this,
new ResultArgs<string>(string.Empty, ex, true, null));
}
_lastError = ex;
}
});
}
Rollback Methods
The last method is RejectStudentChanges()
. Just like
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 the
IClientChangeTracking
interface). Otherwise, the method only
checks whether CurrentStudent
has changes, and if it has, the
method calls RejectObjectGraphChanges()
for
CurrentStudent
only.
/// <summary>
/// If allItems is true, all items from the StudentsList have
/// their changes rejected; otherwise, only CurrentStudent from
/// the StudentsList has its changes rejected.
/// </summary>
/// <param name="allItems"></param>
public void RejectStudentChanges(bool allItems = true)
{
if (allItems)
{
if (StudentsList != null && StudentsListHasChanges)
{
// reject changes for all items from the StudentsList
foreach (var student in StudentsList.Where
(n => n.ObjectGraphHasChanges()).ToList())
{
var isAdded = student.ChangeTracker.State == ObjectState.Added;
student.RejectObjectGraphChanges();
// if the State is Added, simply remove it from the StudentsList
if (isAdded) StudentsList.Remove(student);
}
}
}
else
{
if (CurrentStudent != null && StudentsList != null && CurrentStudentHasChanges)
{
// reject changes for only CurrentStudent from the StudentsList
var currentStudent = StudentsList
.FirstOrDefault(n => n.PersonId == CurrentStudent.PersonId);
if (currentStudent != null)
{
var isAdded = currentStudent.ChangeTracker.State == ObjectState.Added;
currentStudent.RejectObjectGraphChanges();
// if the State is Added, simply remove it from the StudentsList
if (isAdded) StudentsList.Remove(currentStudent);
}
}
}
}
Wrapping Up
We have finished discussing how to use the methods and properties of the
IClientChangeTracking
interface. To summarize, the
ObjectGraphHasChanges()
method is used in multiple places to check
whether an entity has any changes or not. Secondly, the
AcceptObjectGraphChanges()
method is only used when an update
operation completes successfully, while the
RejectObjectGraphChanges()
method is called inside a rollback
operation to revoke any changes made. Lastly, the
GetObjectGraphChanges()
method 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!
History
- August, 2012 - Initial release.
- March, 2013 - Update for version 2.1.3.