Click here to Skip to main content
15,881,577 members
Articles / Desktop Programming / XAML

A Silverlight Sample Built with Self-Tracking Entities and WCF Services - Part 2

Rate me:
Please Sign up or sign in to vote.
4.50/5 (3 votes)
8 Mar 2011CPOL16 min read 35.9K   16   2
Part 2 of a series describing the creation of a Silverlight business application using Self-tracking Entities, WCF Services, WIF, MVVM Light Toolkit, MEF, and T4 Templates.
  • Please visit this project site for the latest releases and source code.

Article Series

This article is the second part of a series on developing a Silverlight business application using Self-tracking Entities, WCF Services, WIF, MVVM Light Toolkit, MEF, and T4 Templates.

Image 1

Contents

Introduction

In this second part, we will go over the topic of how to implement client-side change tracking using self-tracking entities. In the current version of ADO.NET Self-Tracking Entity Generator from VS2010, there is already a method called AcceptChanges(), but there is no implementation of either the method RejectChanges() or the property HasChanges. We will explore how to add these functionalities into our enhanced version of Self-Tracking Entity Generator. After that, we will also go over several topics on how other parts of this sample can seamlessly work with this new data access layer.

Background

Let us first inspect the auto-generated entity classes from the ADO.NET Self-Tracking Entity Generator out of VS2010. Basically, each entity class is a POCO (Plain Old CLR Object) along with two additional interfaces: IObjectWithChangeTracker and INotifyPropertyChanged.

The interface of interest here is IObjectWithChangeTracker, which adds a new property ChangeTracker into each auto-generated entity class. This property keeps all the change tracking information for the subgraph of any given object, and it is of type ObjectChangeTracker.

ObjectChangeTracker is the class where we will find most of the client-side change tracking logic. Let us briefly go over some of the existing methods and properties we may use later:

  • ChangeTrackingEnabled, as its name suggests, stores a Boolean value indicating whether this self-tracking entity object enables change tracking or not.
  • State stores a value of either Unchanged, Added, Modified, or Deleted, which keeps track of the different states of a self-tracking entity object.
  • OriginalValues stores the original values for properties that were changed.
  • ObjectsAddedToCollectionProperties stores the added objects to collection valued properties that were changed.
  • ObjectsRemovedFromCollectionProperties stores the removed objects to collection valued properties that were changed.

In addition to the properties mentioned above, the ObjectChangeTracker class also implements the AcceptChanges() method which we will discuss later.

Change Tracking Infrastructure

After a brief overview of the ObjectChangeTracker class, we are now ready to visit what additional logic we are going to add for a full client-side change tracking infrastructure. Let us first discuss any existing and new events for the ObjectChangeTracker class.

Events ObjectStateChanging, ObjectStateChanged, and UpdateHasChanges

There are three events defined in our new ObjectChangeTracker class, and they are ObjectStateChanging, ObjectStateChanged, and UpdateHasChanges. The ObjectStateChanging event exists in the original version, while the other two are new additions.

C#
public event EventHandler<ObjectStateChangingEventArgs> ObjectStateChanging;
public event EventHandler<ObjectStateChangedEventArgs> ObjectStateChanged;
public event EventHandler UpdateHasChanges;

protected virtual void OnObjectStateChanging(ObjectState newState)
{
  if (ObjectStateChanging != null)
  {
    ObjectStateChanging(this, 
      new ObjectStateChangingEventArgs() { NewState = newState });
  }
}

protected virtual void OnObjectStateChanged(ObjectState newState)
{
  if (ObjectStateChanged != null)
  {
    ObjectStateChanged(this, 
      new ObjectStateChangedEventArgs() { NewState = newState });
  }
}

protected virtual void OnUpdateHasChanges()
{
  if (UpdateHasChanges != null)
  {
    UpdateHasChanges(this, new EventArgs());
  }
}

As their names suggest, the ObjectStateChanging event fires every time before the State property changes, and the ObjectStateChanged event gets triggered every time after the State property changes. The other event UpdateHasChanges is also self-explanatory. It gets fired in places where we want to update the HasChanges property.

Method AcceptChanges()

Next, let us examine the AcceptChanges() method:

C#
// Resets the ObjectChangeTracker to the Unchanged state and
// clears the original values as well as the record of changes
// to collection properties
public void AcceptChanges()
{
  OnObjectStateChanging(ObjectState.Unchanged);
  OriginalValues.Clear();
  ObjectsAddedToCollectionProperties.Clear();
  ObjectsRemovedFromCollectionProperties.Clear();
  ChangeTrackingEnabled = true;
  _objectState = ObjectState.Unchanged;
  OnObjectStateChanged(ObjectState.Unchanged);
}

As the comment above states, the AcceptChanges() clears the property OriginalValues as well as the properties ObjectsAddedToCollectionProperties and ObjectsRemovedFromCollectionProperties. It then resets ObjectChangeTracker back to the Unchanged state, thus accepting all the changes made to the entity object. This method also fires the ObjectStateChanging event before any change takes place, and fires the ObjectStateChanged event immediately after.

Method RejectChanges()

We will discuss RejectChanges() next, but before that, let us briefly go over another simple new method in the class, ObjectChangeTracker.

C#
public void SetParentObject(object parent)
{
  this._parentObject = parent;
}

And, SetParentObject() is used inside the ChangeTracker property of every auto-generated entity class, as follows:

C#
[DataMember]
public ObjectChangeTracker ChangeTracker
{
  get
  {
    if (_changeTracker == null)
    {
      _changeTracker = new ObjectChangeTracker();
      _changeTracker.SetParentObject(this);
      _changeTracker.ObjectStateChanging += HandleObjectStateChanging;
      _changeTracker.ObjectStateChanged += HandleObjectStateChanged;
      _changeTracker.UpdateHasChanges += HandleUpdateHasChanges;
    }
    return _changeTracker;
  }
  set
  {
    if(_changeTracker != null)
    {
      _changeTracker.ObjectStateChanging -= HandleObjectStateChanging;
      _changeTracker.ObjectStateChanged -= HandleObjectStateChanged;
      _changeTracker.UpdateHasChanges -= HandleUpdateHasChanges;
    }
    _changeTracker = value;
    _changeTracker.SetParentObject(this);
    if(_changeTracker != null)
    {
      _changeTracker.ObjectStateChanging += HandleObjectStateChanging;
      _changeTracker.ObjectStateChanged += HandleObjectStateChanged;
      _changeTracker.UpdateHasChanges += HandleUpdateHasChanges;
    }
  }
}

As we can see from the lines of code above, every time ChangeTracker changes, it updates a reference (inside the _parentObject field) to its containing entity object. This _parentObject field is needed by the RejectChanges() method as shown below:

C#
// Resets the ObjectChangeTracker to the Unchanged state and
// rollback the original values as well as the record of changes
// to collection properties
public void RejectChanges()
{
  OnObjectStateChanging(ObjectState.Unchanged);
  // rollback original values
  Type type = _parentObject.GetType();
  foreach (var originalValue in OriginalValues.ToList())
    type.GetProperty(originalValue.Key).SetValue(
         _parentObject, originalValue.Value, null);
  // create copy of ObjectsAddedToCollectionProperties
  // and ObjectsRemovedFromCollectionProperties
  Dictionary<string, ObjectList> removeCollection =
    ObjectsAddedToCollectionProperties.ToDictionary(n => n.Key, n => n.Value);
  Dictionary<string, ObjectList> addCollection =
    ObjectsRemovedFromCollectionProperties.ToDictionary(n => n.Key, n => n.Value);
  // rollback ObjectsAddedToCollectionProperties
  if (removeCollection.Count > 0)
  {
    foreach (KeyValuePair<string, ObjectList> entry in removeCollection)
    {
      PropertyInfo collectionProperty = type.GetProperty(entry.Key);
      IList collectionObject = (IList)collectionProperty.GetValue(_parentObject, null);
      foreach (object obj in entry.Value.ToList())
      {
        collectionObject.Remove(obj);
      }
    }
  }
  // rollback ObjectsRemovedFromCollectionProperties
  if (addCollection.Count > 0)
  {
    foreach (KeyValuePair<string, ObjectList> entry in addCollection)
    {
      PropertyInfo collectionProperty = type.GetProperty(entry.Key);
      IList collectionObject = (IList)collectionProperty.GetValue(_parentObject, null);
      foreach (object obj in entry.Value.ToList())
      {
        collectionObject.Add(obj);
      }
    }
  }
  OriginalValues.Clear();
  ObjectsAddedToCollectionProperties.Clear();
  ObjectsRemovedFromCollectionProperties.Clear();
  _objectState = ObjectState.Unchanged;
  OnObjectStateChanged(ObjectState.Unchanged);
}

RejectChanges() is a bit similar to AcceptChanges(). But in stead of accepting changes, it applies all the original values back with the help of _parentObject and a little magic of .NET reflection. From the code snippet above, we know that it first rolls back all the original values stored in OriginalValues, makes a copy of both ObjectsAddedToCollectionProperties and ObjectsRemovedFromCollectionProperties, and then uses the copies to roll back all those values too. Just like AcceptChanges(), RejectChanges() also fires the ObjectStateChanging event before any changes take place, and fires the ObjectStateChanged event immediately after setting the State back to Unchanged.

Next, we will move on to talk about the new HasChanges property.

Property HasChanges

In WCF RIA Services, the DomainContext class has a property called HasChanges which indicates whether this context has any pending changes. Since we are using self-tracking entities for client-side change tracking, it is logical that we add this new property on each entity class, and we only need to add it on the client-side. In our sample application, this new property is generated by the T4 template IssueVisionClientModel.tt inside the project IssueVision.Data.

C#
public Boolean HasChanges
{
  get { return _hasChanges; }
  private set
  {
    if (_hasChanges != value)
    {
      _hasChanges = value;
      if (_propertyChanged != null)
      {
        _propertyChanged(this, 
          new PropertyChangedEventArgs("HasChanges"));
      }
    }
  }
}
private Boolean _hasChanges = true;

Please note that it is important to set the initial value of this property to true. This is because whenever we create a new entity object, its initial State is set as Added. Since any entity object in Added state always has changes to save, the initial value of HasChanges should be true.

Next, let us try to figure out where the HasChanges property needs to get updated. As we have already discussed above, there are three events defined inside the ObjectChangeTracker class: ObjectStateChanging, ObjectStateChanged, and UpdateHasChanges. It is easy to figure out that we need to update HasChanges whenever either the ObjectStateChanged or UpdateHasChanges event gets triggered.

C#
private void HandleObjectStateChanged(object sender, ObjectStateChangedEventArgs e)
{
#if SILVERLIGHT
  // update HasChanges property
  HasChanges = (this.ChangeTracker.State == ObjectState.Added) ||
    (this.ChangeTracker.ChangeTrackingEnabled &&
    (this.ChangeTracker.State != ObjectState.Unchanged ||
     this.ChangeTracker.ObjectsAddedToCollectionProperties.Count != 0 ||
     this.ChangeTracker.ObjectsRemovedFromCollectionProperties.Count != 0));
#endif
}

private void HandleUpdateHasChanges(object sender, EventArgs e)
{
#if SILVERLIGHT
  // update HasChanges property
  HasChanges = (this.ChangeTracker.State == ObjectState.Added) ||
    (this.ChangeTracker.ChangeTrackingEnabled &&
    (this.ChangeTracker.State != ObjectState.Unchanged ||
     this.ChangeTracker.ObjectsAddedToCollectionProperties.Count != 0 ||
     this.ChangeTracker.ObjectsRemovedFromCollectionProperties.Count != 0));
#endif
}

The logic to determine whether there is any pending change for a specific self-tracking entity object is as follows: first, if the State of ChangeTracker is Added, the entity object has pending changes. Second, if change tracking is enabled, and its State is not Unchanged or either ObjectsAddedToCollectionProperties or ObjectsRemovedFromCollectionProperties is not empty, the entity object also has pending changes.

The ObjectStateChanged event is triggered by the OnObjectStateChanged() method, and this method is called in both AcceptChanges() and RejectChanges(), as we have already seen above. The UpdateHasChanges event is triggered by the OnUpdateHasChanges() method, and it is called in places like the following:

C#
public bool ChangeTrackingEnabled
{
  get { return _changeTrackingEnabled; }
  set
  {
    if (_changeTrackingEnabled != value)
    {
      _changeTrackingEnabled = value;
      OnUpdateHasChanges();
    }
  }
}

So far, we have completed our discussion about this new change tracking infrastructure. Next, we will move on to talk about how to use these new features.

Class IssueVisionModel Implementation

Let us first take a look at how the IssueVisionModel class is built. The IssueVisionModel class implements the IIssueVisionModel interface in the project IssueVision.Common, and if you are familiar with the previous sample built with WCF RIA Services, you will notice that this interface class looks very similar in both samples. For example, they both have methods like SaveChangesAsync(), RejectChanges(), and both have properties like HasChanges, IsBusy, etc. Actually, this is a good thing. It means that even if these two samples use totally different data access layers, the model classes expose almost identical interfaces, thus making it possible that we can re-use most of the source code from the View and ViewModel classes.

Before we dig into the IssueVisionModel class, let us first take a look at the IssueVision.WCFService project where we will find all the service references.

Project IssueVision.WCFService

Both the IssueVisionServiceClient and PasswordResetServiceClient classes derive from the base class ClientBase, which provides the base implementation used to create client objects that can call services. For each of these two classes, we have implemented the Singleton pattern, and added a new property called ActiveCallCount that keeps track of the number of concurrent active calls.

C#
#region "Singleton"
private static readonly IssueVisionServiceClient instance = 
        new IssueVisionServiceClient("CustomBinding_IIssueVisionService");

public static IssueVisionServiceClient Instance
{
  get { return instance; }
}
#endregion "Singleton"

#region "Active Call Count"
private int _activeCallCount;
public int ActiveCallCount
{
  get { return this._activeCallCount; }
}

public void DecrementCallCount()
{
  Interlocked.Decrement(ref this._activeCallCount);
  if (this._activeCallCount == 0)
    this.OnPropertyChanged("ActiveCallCount");
}

public void IncrementCallCount()
{
  Interlocked.Increment(ref this._activeCallCount);
  if (this._activeCallCount == 1)
    this.OnPropertyChanged("ActiveCallCount");
}
#endregion "Active Call Count"

Because of the Singleton design pattern and this new property ActiveCallCount, we are now able to implement the IsBusy property.

Property IsBusy

IsBusy is defined as follows:

C#
/// <summary>
/// True if at least one call is
/// in progress; otherwise, false
/// </summary>
public Boolean IsBusy
{
  get { return this._isBusy; }
  private set
  {
    if (this._isBusy != value)
    {
      this._isBusy = value;
      this.OnPropertyChanged("IsBusy");
    }
  }
}

private Boolean _isBusy = false;

/// <summary>
/// Event handler for PropertyChanged
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void _proxy_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
  if (e.PropertyName.Equals("ActiveCallCount"))
  {
    // re-calculate IsBusy
    this.IsBusy = (this._proxy.ActiveCallCount != 0);
  }
}

The value of IsBusy is updated every time there is a PropertyChanged event for ActiveCallCount. If ActiveCallCount is not equal to zero, IsBusy is set to true; otherwise, it is set to false.

Property HasChanges

Next, let us check how to implement the HasChanges property. Basically, this property is set to true if there is any pending change for any self-tracking entity object the Model class is keeping track of. Based on the business logic of this sample application, we will only update either an Issue object or a User object. Therefore, we add two new properties called CurrentEditIssue and CurrentEditUser into the IssueVisionModel class. Following is the code snippet for the CurrentEditUser property:

C#
/// <summary>
/// Keeps a reference to the current User
/// item in edit
/// </summary>
public User CurrentEditUser
{
  get { return _currentEditUser; }
  set
  {
    if (!this.CurrentEditUserHasChanges())
    {
      if (!ReferenceEquals(_currentEditUser, value))
      {
        if (_currentEditUser != null)
        {
          ((INotifyPropertyChanged)_currentEditUser).PropertyChanged -= 
                                    IssueVisionModel_PropertyChanged;
        }
        _currentEditUser = value;
        if (_currentEditUser != null)
        {
          ((INotifyPropertyChanged)_currentEditUser).PropertyChanged += 
                                   IssueVisionModel_PropertyChanged;
        }
        ReCalculateHasChanges();
      }
    }
    else
      throw new InvalidOperationException(CommonResources.HasChangesIsTrue);
  }
}

private User _currentEditUser;

/// <summary>
/// Event handler for PropertyChanged
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void IssueVisionModel_PropertyChanged(object sender, 
             PropertyChangedEventArgs e)
{
  if (e.PropertyName.Equals("HasChanges"))
  {
    ReCalculateHasChanges();
  }
}

As the code above shows, the CurrentEditUser property subscribes to the PropertyChanged event, and whenever this self-tracking entity object fires a PropertyChanged event of its property HasChanges, the HasChanges property on the Model class gets re-calculated with the ReCalculateHasChanges() method. Please do not get confused with the two different HasChanges properties. One is defined on the entity class level, and the other is defined inside the Model class IssueVisionModel. The ReCalculateHasChanges() method is defined below:

C#
/// <summary>
/// Function to re-calculate HasChanges based on
/// the values of _currentEditIssue and _currentEditUser
/// </summary>
private void ReCalculateHasChanges()
{
  // re-calculate HasChanges for both CurrentEditIssue and CurrentEditUser
  this.HasChanges = CurrentEditIssueHasChanges() || CurrentEditUserHasChanges();
}

/// <summary>
/// Function to re-calculate HasChanges of _currentEditIssue.
/// This function checks HasChanges of the _currentEditIssue
/// itself as well as all its Navigation properties.
/// </summary>
/// <returns></returns>
private bool CurrentEditIssueHasChanges()
{
  Boolean hasChanges = false;
  if (_currentEditIssue != null)
  {
    hasChanges = hasChanges || _currentEditIssue.HasChanges ||
      (_currentEditIssue.Platform == null ? false : 
       _currentEditIssue.Platform.HasChanges) ||
      (_currentEditIssue.Attributes == null ? false : 
       _currentEditIssue.Attributes.Any(n => n.HasChanges)) ||
      (_currentEditIssue.Files == null ? false : 
       _currentEditIssue.Files.Any(n => n.HasChanges));
  }
  return hasChanges;
}

/// <summary>
/// Function to re-calculate HasChanges of _currentEditUser.
/// </summary>
/// <returns></returns>
private bool CurrentEditUserHasChanges()
{
  Boolean hasChanges = false;
  if (_currentEditUser != null)
  {
    hasChanges = hasChanges || _currentEditUser.HasChanges;
  }
  return hasChanges;
}

The HasChanges property is set to true if either the CurrentEditIssueHasChanges() or CurrentEditUserHasChanges() function returns true. And, the CurrentEditIssueHasChanges() function checks whether the entity object CurrentEditIssue has any pending changes. This is accomplished by checking whether the object itself has any pending changes, as well as looping through all its navigation properties, namely Platform, Attributes, and Files. The CurrentEditUserHasChanges() function performs almost identical logic.

Method SaveChangesAsync()

As we now know how the HasChanges property works, we can move on to talk about the SaveChangesAsync() method.

C#
/// <summary>
/// Save changes on both
/// CurrentEditIssue and CurrentEditUser
/// </summary>
public void SaveChangesAsync()
{
  if (HasChanges)
  {
    if (_currentEditIssue != null && CurrentEditIssueHasChanges())
    {
      this._proxy.UpdateIssueAsync(_currentEditIssue);
      this._proxy.IncrementCallCount();
      this._updateIssueDone = false;
    }
    if (_currentEditUser != null && CurrentEditUserHasChanges())
    {
      this._proxy.UpdateUserAsync(_currentEditUser);
      this._proxy.IncrementCallCount();
      this._updateUserDone = false;
    }
  }
}

SaveChangesAsync() first checks whether there are any pending changes. If that is true, the method will further verify whether there are any pending changes for the CurrentEditIssue property. If that is also true, an actual call to UpdateIssueAsync() is made passing in the object _currentEditIssue. Next, the method takes a similar approach against the property CurrentEditUser.

After either UpdateIssueAsync() or UpdateUserAsync() is done on the server side, the _proxy_UpdateIssueCompleted() or _proxy_UpdateUserCompleted() event handler will get called:

C#
/// <summary>
/// Event handler for UpdateIssueCompleted
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void _proxy_UpdateIssueCompleted(object sender, 
             UpdateIssueCompletedEventArgs e)
{
  this._proxy.DecrementCallCount();
  this._updateIssueDone = true;

  string warningMessage = string.Empty;
  long updatedIssueID = 0;
  if (e.Error == null)
  {
    if (e.Result.Count() == 2)
    {
      warningMessage = e.Result[0] as string;
      updatedIssueID = Convert.ToInt64(e.Result[1]);
    }
  }

  if (e.Error == null && string.IsNullOrEmpty(warningMessage))
  {
    // first check whether this is an update of a new issue
    if (_currentEditIssue.ChangeTracker.State == ObjectState.Added)
    {
      // get the new issue ID returned
      _currentEditIssue.IssueID = updatedIssueID;
    }
    // if there is no error, call AcceptChanges() first
    // we need to call AcceptChanges() on the issue itself
    // as well as all its Navigation properties
    _currentEditIssue.AcceptChanges();
    if (_currentEditIssue.Platform != null)
    {
      _currentEditIssue.Platform.AcceptChanges();
    }
    if (_currentEditIssue.Attributes != null)
    {
      foreach (IssueVision.EntityModel.Attribute item in _currentEditIssue.Attributes)
        item.AcceptChanges();
    }
    if (_currentEditIssue.Files != null)
    {
      foreach (IssueVision.EntityModel.File item in _currentEditIssue.Files)
        item.AcceptChanges();
    }
  }
  else
  {
    // if there is an error, we need to send notification on first occurrence,
    // in other words, if _lastError is still null
    if (SaveChangesCompleted != null)
    {
      if (this._lastError == null || this.AllowMultipleErrors)
      {
        SaveChangesCompleted(this, new ResultArgs<string>(
                             warningMessage, e.Error, e.Cancelled, e.UserState));
      }
    }
    this._lastError = e.Error;
  }

  // we need to send notification when both _updateIssueDone
  // and _updateUserDone are true, and if there is no error.
  if (this._updateIssueDone && this._updateUserDone)
  {
    if (SaveChangesCompleted != null && this._lastError == 
               null && string.IsNullOrEmpty(warningMessage))
    {
      SaveChangesCompleted(this, new ResultArgs<string>(
         string.Empty, e.Error, e.Cancelled, e.UserState));
    }
  }
}

The event handler for UpdateIssueCompleted shown above first checks whether there is any error or warning message returned from the server. If everything works OK, it calls AcceptChanges() on _currentEditIssue as well as all its navigation properties, which sets the HasChanges property of _currentEditIssue back to false. But if something goes wrong, AcceptChanges() will not be called, and the error or warning message will be passed back to any ViewModel class through an event. And, if a user chooses to cancel any failed update operation, a RejectChanges() will be made as we will discuss next.

Method RejectChanges()

It is relatively easy to understand RejectChanges() because it is similar in logic to the SaveChangesAsync() method.

C#
/// <summary>
/// Call RejectChanges on both
/// CurrentEditIssue and CurrentEditUser
/// </summary>
public void RejectChanges()
{
  if (_currentEditIssue != null)
  {
    _currentEditIssue.RejectChanges();
    if (_currentEditIssue.Attributes != null)
    {
      foreach (IssueVision.EntityModel.Attribute item in _currentEditIssue.Attributes)
        item.RejectChanges();
    }
    if (_currentEditIssue.Files != null)
    {
      foreach (IssueVision.EntityModel.File item in _currentEditIssue.Files)
        item.RejectChanges();
    }
  }
  if (_currentEditUser != null)
  {
    _currentEditUser.RejectChanges();
  }
}

The RejectChanges() method on the Model class will call RejectChanges() on both CurrentEditIssue and CurrentEditUser. And, for each entity, RejectChanges() gets called on the object itself as well as all its navigation properties that are collection objects.

Here we finish our discussion on how the Model class IssueVisionModel is built; we will move on to a new topic about different categories of entity properties.

Three Different Categories of Entity Properties

In WCF RIA Services, we can tweak the accessibility of a certain entity property by using either metadata annotations or partial classes. Take the User class as an example; it is defined in the EDM file like the following:

Image 2

The auto-generated entity class is based on this EF Model, and it is a direct reflection of what is available in the database. But with WCF RIA Services, we can further tweak this class like the following:

C#
[MetadataTypeAttribute(typeof(User.UserMetadata))]
public partial class User
{
  internal class UserMetadata
  {
    // Metadata classes are not meant to be instantiated.
    protected UserMetadata()
    {
    }
    ......

    [Exclude]
    public string PasswordAnswerHash { get; set; }

    [Exclude]
    public string PasswordAnswerSalt { get; set; }

    [Exclude]
    public string PasswordHash { get; set; }

    [Exclude]
    public string PasswordSalt { get; set; }

    [Exclude]
    public Byte ProfileReset { get; set; }
  }
  ......
  [DataMember]
  public string Password { get; set; }

  ......
  [DataMember]
  public string NewPassword { get; set; }
  ......
}

The code snippet above basically tells WCF RIA Services to exclude generating properties like PasswordSalt and PasswordHash on the client-side, and add two new properties Password and NewPassword into the entity class User, and also generate those two properties on the client-side. Please note that these two new properties do not exist in our sample database. With this type of flexibility, we can classify entity properties into the following three categories:

  1. Properties that are only available on the client-side but not on the server-side.
  2. Properties that are available on both client and server sides, including properties that can be directly saved into a database field, as well as properties that cannot, such as Password and NewPassword above.
  3. Properties that are only available on the server-side but never generated on the client-side, like PasswordSalt and PasswordHash above.

Unfortunately, if we choose self-tracking entitles and WCF Services as our data access layer, most of these nice features as you see above are not available, and we have to do things a little bit differently.

Properties Available Only on Client-side

Let us start with an easy case first. For properties available only on client-side, we can simply take the same approach as we do with WCF RIA Services: adding new properties on client-side by using partial classes:

C#
/// <summary>
/// User class client-side extensions
/// </summary>
public partial class User
{
  ......
  [Display(Name = "Confirm new password")]
  [Required(ErrorMessage = "This field is required.")]
  [CustomValidation(typeof(User), "CheckNewPasswordConfirmation")]
  public string NewPasswordConfirmation
  {
    get
    {
      return this._newPasswordConfirmation;
    }

    set
    {
      PropertySetterEntry("NewPasswordConfirmation");
      _newPasswordConfirmation = value;
      PropertySetterExit("NewPasswordConfirmation", value);
      OnPropertyChanged("NewPasswordConfirmation");
    }
  }
  private string _newPasswordConfirmation;
  ......
}

Properties Available on Both Sides

It seems easy to handle properties available on both sides until we need to exclude properties like PasswordHash and add new properties like NewPassword so that they are available on both sides even though there is no such database field that ever existed.

Image 3

Our approach is to use an advanced feature of Entity Framework called "virtual table". In case you are not familiar, here is a link to the MSDN documentation. Next, we will walk through how to add User as a virtual table into our EDM file.

  1. First, we need to open the "IssueVision.edmx" file with an XML editor by right-clicking on the file, select Open With, then choose XML Editor, and click OK.

    Image 4

  2. Add an EntitySet and DefiningQuery element into the SSDL section, as follows:
    XML
    <EntitySet Name="Users" 
          EntityType="IssueVisionModel.Store.Users" 
          store:Type="Views">
      <DefiningQuery>
        <![CDATA[
          SELECT [Name]
          ,[FirstName]
          ,[LastName]
          ,[Email]
          ,'' AS Password
          ,'' AS NewPassword
          ,[PasswordQuestion]
          ,'' AS PasswordAnswer
          ,[UserType]
          ,[ProfileReset]
          ,CAST(0 AS tinyint) AS IsUserMaintenance
          FROM [Users]
        ]]>
      </DefiningQuery>
    </EntitySet>
  3. Add an EntityType in the section where EntityType items are defined within the SSDL section:
    XML
    <EntityType Name="Users">
      <Key>
        <PropertyRef Name="Name" />
      </Key>
      <Property Name="Name" Type="nvarchar" 
        Nullable="false" MaxLength="50" />
      <Property Name="FirstName" Type="nvarchar" 
        Nullable="false" MaxLength="50" />
      <Property Name="LastName" Type="nvarchar" 
        Nullable="false" MaxLength="50" />
      <Property Name="Email" 
        Type="nvarchar" MaxLength="100" />
      <Property Name="Password" Type="nvarchar" 
        Nullable="false" MaxLength="50" />
      <Property Name="NewPassword" Type="nvarchar" 
        Nullable="false" MaxLength="50" />
      <Property Name="PasswordQuestion" Type="nvarchar" 
        Nullable="false" MaxLength="200" />
      <Property Name="PasswordAnswer" Type="nvarchar" 
        Nullable="false" MaxLength="200" />
      <Property Name="UserType" Type="char" 
        Nullable="false" MaxLength="1" />
      <Property Name="ProfileReset" 
        Type="tinyint" Nullable="false" />
      <Property Name="IsUserMaintenance" 
        Type="tinyint" Nullable="false" />
    </EntityType>
  4. Add an EntitySet element into the CSDL section as follows:
    XML
    <EntitySet Name="Users" EntityType="IssueVisionModel.User" />
  5. Add an EntityType in the section where EntityType items are defined within the CSDL section:
    XML
    <EntityType Name="User">
      <Key>
        <PropertyRef Name="Name" />
      </Key>
      <Property Name="Name" Type="String" Nullable="false" 
         MaxLength="50" Unicode="true" FixedLength="false" />
      <Property Name="FirstName" Type="String" Unicode="true" 
         FixedLength="false" MaxLength="50" Nullable="false" />
      <Property Name="LastName" Type="String" Unicode="true" 
         FixedLength="false" MaxLength="50" Nullable="false" />
      <Property Name="Email" Type="String" Unicode="true" 
         FixedLength="false" MaxLength="100" />
      <Property Name="Password" Type="String" Unicode="true" 
        FixedLength="false" MaxLength="50" Nullable="false" />
      <Property Name="NewPassword" Type="String" Unicode="true" 
        FixedLength="false" MaxLength="50" Nullable="false" />
      <Property Name="PasswordQuestion" Type="String" 
        Unicode="true" FixedLength="false" 
        MaxLength="200" Nullable="false" />
      <Property Name="PasswordAnswer" Type="String" 
        Unicode="true" FixedLength="false" 
        MaxLength="200" Nullable="false" />
      <Property Name="UserType" Type="String" 
        Unicode="false" FixedLength="true" 
        MaxLength="1" Nullable="false" />
      <Property Type="Byte" Name="ProfileReset" 
        Nullable="false" />
      <Property Type="Byte" Name="IsUserMaintenance" 
        Nullable="false" />
    </EntityType>
  6. Save our changes and switch back to the Designer so that we can map the entity to the virtual table we just created, and it should look like the following:

    Image 5

  7. Next, we are going to add three custom functions into the SSDL section for the insert/delete/update operations of our newly created User entity. In case you need further information on how to define custom functions in the storage model, here is the link to the MSDN documentation. Following is one of the three custom functions:
    XML
    <Function Name="UpdateUser" IsComposable="false">
      <CommandText>
        <![CDATA[
          UPDATE [Users]
          SET [FirstName] = @FirstName
          ,[LastName] = @LastName
          ,[Email] = @Email
          ,[PasswordQuestion] = @PasswordQuestion
          ,[UserType] = @UserType
          ,[ProfileReset] = @ProfileReset
          WHERE [Name] = @Name
        ]]>
      </CommandText>
      <Parameter Name="Name" Type="nvarchar" 
        MaxLength="50" Mode="In"/>
      <Parameter Name="FirstName" Type="nvarchar" 
        MaxLength="50" Mode="In"/>
      <Parameter Name="LastName" Type="nvarchar" 
        MaxLength="50" Mode="In"/>
      <Parameter Name="Email" Type="nvarchar" 
        MaxLength="100" Mode="In"/>
      <Parameter Name="PasswordQuestion" Type="nvarchar" 
        MaxLength="200" Mode="In"/>
      <Parameter Name="UserType" Type="char" 
        MaxLength="1" Mode="In"/>
      <Parameter Name="ProfileReset" Type="tinyint" Mode="In"/>
    </Function>
  8. Again, save our changes after adding these three custom functions and switch back to the Designer so that we can map the entity and custom functions.

    Image 6

The advantage of using virtual tables is that the entity properties are not necessarily database fields, and even the entity class itself does not have to be based on one database table. It could be a joint of multiple tables. Also, there is no requirement to define matching insert/delete/update custom functions. Just like the entity class PasswordResetUser in our sample application, it essentially becomes a read-only view.

Lastly, one cautionary note of using virtual tables is that none of these things actually exist in our sample database. If you run the Update Model Wizard, Entity Framework 4.0 Designer will wipe out any customizations of the SSDL. So, keep a copy of these changes if you need to update the EF model.

Properties Available Only on Server-side

Next, we are going to discuss how to implement the last of the three different categories of entity properties. In our sample application, the server-side only properties are PasswordSalt, PasswordHash, PasswordAnswerSalt, and PasswordAnswerHash. It is obvious that these four properties should stay on the server-side as transferring them to the client-side may become a security leak. One approach to keep properties on server-side is to take them out of the entity class User and PasswordResetUser of the EF Model, and use function imports to retrieve and update those database fields. Here is a link from MSDN documentation, in case you are not familiar with how to create a function import.

First, open the "IssueVision.edmx" file with the XML editor, and add all the related custom functions into the SSDL section. Following is just one of them:

XML
<Function Name="GetPasswordAnswerHash" IsComposable="false">
  <CommandText>
    <![CDATA[
      SELECT
      [PasswordAnswerHash]
      FROM [Users]
      WHERE Name = @Name
    ]]>
  </CommandText>
  <Parameter Name="Name" Type="nvarchar" 
     MaxLength="50" Mode="In"/>
</Function>

After that, add function imports using the "Add Function Import" dialog box, as follows:

Image 7

We need to add a total of six function imports as the following "Model Browser" shows:

Image 8

Let us take a look at how to use these new function imports to retrieve and update server-side only properties. For example, context.GetPasswordAnswerHash(user.Name).First() will retrieve the PasswordAnswerHash field from the User table based on a certain user name. And, we can use context.ExecuteFunction to call UpdatePasswordHashAndSalt with the parameters of password salt and password hash values. The ResetPassword() method in the class PasswordResetService demonstrates how they are being used:

C#
/// <summary>
/// Reset user password if security question and security
/// answer match
/// </summary>
/// <param name="user"></param>
public void ResetPassword(PasswordResetUser user)
{
  // validate the user on the server side first
  user.Validate();

  using (IssueVisionEntities context = new IssueVisionEntities())
  {
    User foundUser = context.Users.FirstOrDefault(n => n.Name == user.Name);

    if (foundUser != null)
    {
      // retrieve password answer hash and salt from database
      string currentPasswordAnswerHash = context.GetPasswordAnswerHash(user.Name).First();
      string currentPasswordAnswerSalt = context.GetPasswordAnswerSalt(user.Name).First();
      // generate password answer hash
      string passwordAnswerHash = HashHelper.ComputeSaltedHash(user.PasswordAnswer,
      currentPasswordAnswerSalt);

      if (string.Equals(user.PasswordQuestion, 
                        foundUser.PasswordQuestion, 
                        StringComparison.Ordinal) &&
          string.Equals(passwordAnswerHash, currentPasswordAnswerHash, 
                        StringComparison.Ordinal))
      {
        // Password answer matches, so save the new user password
        // Re-generate password hash and password salt
        string currentPasswordSalt = HashHelper.CreateRandomSalt();
        string currentPasswordHash = 
          HashHelper.ComputeSaltedHash(user.NewPassword, currentPasswordSalt);

        // re-generate passwordAnswer hash and passwordAnswer salt
        currentPasswordAnswerSalt = HashHelper.CreateRandomSalt();
        currentPasswordAnswerHash = 
          HashHelper.ComputeSaltedHash(user.PasswordAnswer, currentPasswordAnswerSalt);

        // save changes
        context.ExecuteFunction("UpdatePasswordHashAndSalt"
          , new ObjectParameter("Name", user.Name)
          , new ObjectParameter("PasswordHash", currentPasswordHash)
          , new ObjectParameter("PasswordSalt", currentPasswordSalt));
        context.ExecuteFunction("UpdatePasswordAnswerHashAndSalt"
          , new ObjectParameter("Name", user.Name)
          , new ObjectParameter("PasswordAnswerHash", currentPasswordAnswerHash)
          , new ObjectParameter("PasswordAnswerSalt", currentPasswordAnswerSalt));
      }
      else
        throw new UnauthorizedAccessException(ErrorResources.PasswordQuestionDoesNotMatch);
    }
    else
      throw new UnauthorizedAccessException(ErrorResources.NoUserFound);
  }
}

Retrieving and updating server-side only properties with function imports makes sure that none of these properties are exposed on the client-side. One problem I can think of is that this approach may not scale well if we have lots of server-side only properties. But, for most LOB applications, the number of server-side only properties should be relatively small, while the majority should be properties available on both sides. Therefore, scalability should not be a big concern here.

Server-side Update Logic

Before we finish this article, our last topic is how we actually do add/delete/update operations on the server-side. Following is how the UpdateIssue() method of the IssueVisionService class gets implemented.

C#
public List<object> UpdateIssue(Issue issue)
{
  List<object> returnList = new List<object>();

  using (IssueVisionEntities context = new IssueVisionEntities())
  {
    if (issue.ChangeTracker.State == ObjectState.Added)
    {
      // this is inserting a new issue
      // repeat the client-side validation on the server side first
      issue.Validate();

      issue.OpenedDate = DateTime.Now;
      issue.OpenedByID = HttpContext.Current.User.Identity.Name;
      issue.LastChange = DateTime.Now;
      issue.ChangedByID = HttpContext.Current.User.Identity.Name;
      long newIssueID = context.Issues.Count() > 0 ? 
        (from iss in context.Issues select iss.IssueID).Max() + 1 : 1;
      long newIssueHistoryID = context.IssueHistories.Count() > 0 ? 
          (from iss in context.IssueHistories select iss.IssueID).Max() + 1 : 1;
      // create a new Issue ID based on Issues and IssueHistories tables
      issue.IssueID = newIssueHistoryID > newIssueID ? newIssueHistoryID : newIssueID;
      // if status is Open, AssignedToID should be null
      if (issue.StatusID == IssueVisionServiceConstant.OpenStatusID)
      {
        issue.AssignedToID = null;
      }
      // set ResolutionDate and ResolvedByID based on ResolutionID
      if (issue.ResolutionID == null || issue.ResolutionID == 0)
      {
        issue.ResolutionDate = null;
        issue.ResolvedByID = null;
      }
      else
      {
        issue.ResolutionDate = DateTime.Now;
        issue.ResolvedByID = HttpContext.Current.User.Identity.Name;
      }

      // saving changes
      context.Issues.ApplyChanges(issue);
      context.SaveChanges();
      // return the new IssueID created
      returnList.Add(string.Empty);
      returnList.Add(issue.IssueID);
      return returnList;
    }
    else if (issue.ChangeTracker.State == ObjectState.Deleted)
    {
      // this is deleting an issue
      if (issue.StatusID == IssueVisionServiceConstant.ActiveStatusID)
      {
        // cannot delete an active issue
        returnList.Add(ErrorResources.IssueWithActiveStatusID);
        returnList.Add(0);
        return returnList;
      }
      if (!HttpContext.Current.User.IsInRole(IssueVisionServiceConstant.UserTypeAdmin) &&
          !(HttpContext.Current.User.Identity.Name.Equals(issue.AssignedToID)) &&
          !(issue.AssignedToID == null && 
            HttpContext.Current.User.Identity.Name.Equals(issue.OpenedByID)))
      {
        // no permission to delete this issue
        returnList.Add(ErrorResources.NoPermissionToDeleteIssue);
        returnList.Add(0);
        return returnList;
      }

      // saving changes
      context.Issues.ApplyChanges(issue);
      context.SaveChanges();
      // return 0 for delete operation
      returnList.Add(string.Empty);
      returnList.Add(0);
      return returnList;
    }
    else
    {
      // this is updating an issue and its navigation properties
      // repeat the client-side validation on the server side first
      issue.Validate();
      // retrieve the original values
      Issue originalIssue;
      using (IssueVisionEntities otherContext = new IssueVisionEntities())
      {
        originalIssue = otherContext.Issues.First(n => n.IssueID == issue.IssueID);
      }
      // Business logic:
      // Admin user can read/update any issue, and
      // normal user can only read/update issues assigned to them
      // or issues created by them and have not assigned to anyone.
      if (!IssueIsReadOnly(originalIssue))
      {
        issue.LastChange = DateTime.Now;
        issue.ChangedByID = HttpContext.Current.User.Identity.Name;

        // saving changes
        context.Issues.ApplyChanges(issue);
        context.SaveChanges();
        // return the IssueID of the updated issue
        returnList.Add(string.Empty);
        returnList.Add(issue.IssueID);
        return returnList;
      }
      else
      {
        returnList.Add(ErrorResources.NoPermissionToUpdateIssue);
        returnList.Add(0);
        return returnList;
      }
    }
  }
}

As the method above demonstrates, we determine whether to add, delete, or update an issue entity based on the ChangeTracker's State property. If the State is Added, we are going to add a new issue. If the State is Deleted, we should delete that issue from the database. And if the State is either Unchanged or Modified, we will do an update operation. Also, no matter whether it is an add, delete, or update operation, we save changes by simply calling context.Issues.ApplyChanges(issue) followed by context.SaveChanges().

Next Steps

We have covered quite a few topics in this second article. For our next part, we will move on to discuss the topics of client and server side validation logic with self-tracking entities. I hope you find this article useful, and please rate and/or leave feedback below. Thank you!

History

  • January, 2011 - Initial release.
  • March, 2011 - Update to fix multiple bugs including memory leak issues.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
Weidong has been an information system professional since 1990. He has a Master's degree in Computer Science, and is currently a MCSD .NET

Comments and Discussions

 
Questionproblem on self tracking on change of property of TrackableCollection Pin
Giovanni Caputo6-Feb-12 22:32
Giovanni Caputo6-Feb-12 22:32 
AnswerRe: problem on self tracking on change of property of TrackableCollection Pin
Weidong Shen7-Feb-12 15:48
Weidong Shen7-Feb-12 15:48 
I have not updated this sample for quite some time. For a more recent sample, I would suggest you to download either SchoolSample or OrderIT sample from the download section of here[^].

Thanks!

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.