65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Self-Tracking Entities 和 WCF 服务构建的 Silverlight 示例 - 第 2 部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.50/5 (3投票s)

2011年1月18日

CPOL

16分钟阅读

viewsIcon

37483

downloadIcon

5

本系列文章的第二部分,介绍使用自跟踪实体、WCF服务、WIF、MVVM Light Toolkit、MEF和T4模板创建Silverlight业务应用程序。

  • 请访问此 项目站点 获取最新版本和源代码。

文章系列

本文是关于使用自跟踪实体、WCF服务、WIF、MVVM Light Toolkit、MEF和T4模板开发Silverlight业务应用程序的系列文章的第二部分。

目录

引言

在第二部分中,我们将介绍如何使用自跟踪实体实现客户端更改跟踪。在VS2010的ADO.NET自跟踪实体生成器的当前版本中,已经有一个名为AcceptChanges()的方法,但是没有实现RejectChanges()方法或HasChanges属性。我们将探讨如何在我们的增强型自跟踪实体生成器版本中添加这些功能。之后,我们还将介绍此示例的其他部分如何与这个新的数据访问层无缝协同工作的几个主题。

背景

让我们首先检查一下ADO.NET自跟踪实体生成器(来自VS2010)自动生成的实体类。基本上,每个实体类都是一个POCO(纯粹的CLR对象),并附带两个附加接口:IObjectWithChangeTrackerINotifyPropertyChanged

这里感兴趣的接口是IObjectWithChangeTracker,它为每个自动生成的实体类添加了一个新的ChangeTracker属性。此属性存储给定对象的子图的所有更改跟踪信息,其类型为ObjectChangeTracker

ObjectChangeTracker是我们找到大部分客户端更改跟踪逻辑的类。让我们简要回顾一下我们以后可能会用到的一些现有方法和属性。

  • ChangeTrackingEnabled顾名思义,它存储一个布尔值,指示此自跟踪实体对象是否启用更改跟踪。
  • State存储UnchangedAddedModifiedDeleted之一的值,用于跟踪自跟踪实体对象的不同状态。
  • OriginalValues存储已更改属性的原始值。
  • ObjectsAddedToCollectionProperties存储已更改的集合属性中添加的对象。
  • ObjectsRemovedFromCollectionProperties存储已更改的集合属性中删除的对象。

除了上面提到的属性之外,ObjectChangeTracker类还实现了AcceptChanges()方法,我们将在后面讨论。

更改跟踪基础结构

在简要概述了ObjectChangeTracker类之后,我们现在可以开始讨论我们将要为完整的客户端更改跟踪基础结构添加哪些附加逻辑。让我们先讨论ObjectChangeTracker类中的现有和新事件。

事件ObjectStateChanging、ObjectStateChanged和UpdateHasChanges

我们的新ObjectChangeTracker类定义了三个事件:ObjectStateChangingObjectStateChangedUpdateHasChangesObjectStateChanging事件存在于原始版本中,而另外两个是新增的。

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());
  }
}

顾名思义,ObjectStateChanging事件在State属性更改之前触发,而ObjectStateChanged事件在State属性更改之后触发。另一个事件UpdateHasChanges也不言自明。它在我们需要更新HasChanges属性的地方触发。

方法AcceptChanges()

接下来,让我们检查一下AcceptChanges()方法。

// 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);
}

如上面的注释所述,AcceptChanges()会清除OriginalValues属性以及ObjectsAddedToCollectionPropertiesObjectsRemovedFromCollectionProperties属性。然后将ObjectChangeTracker重置为Unchanged状态,从而接受对实体对象所做的所有更改。此方法还在任何更改发生之前触发ObjectStateChanging事件,并在之后立即触发ObjectStateChanged事件。

方法RejectChanges()

我们接下来将讨论RejectChanges(),但在此之前,让我们简要回顾一下ObjectChangeTracker类中另一个简单的¡新方法。

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

并且,SetParentObject()在每个自动生成的实体类的ChangeTracker属性中使用,如下所示:

[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;
    }
  }
}

从上面的代码行可以看出,每次ChangeTracker更改时,它都会更新指向其包含实体对象的引用(在_parentObject字段中)。这个_parentObject字段是RejectChanges()方法所必需的,如下所示:

// 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()AcceptChanges()有点相似。但不是接受更改,而是使用_parentObject和一些.NET反射的魔力将所有原始值应用回去。从上面的代码片段中,我们知道它首先回滚OriginalValues中存储的所有原始值,然后复制ObjectsAddedToCollectionPropertiesObjectsRemovedFromCollectionProperties,最后使用副本回滚所有这些值。与AcceptChanges()一样,RejectChanges()也在任何更改发生之前触发ObjectStateChanging事件,并在将State设置回Unchanged后立即触发ObjectStateChanged事件。

接下来,我们将继续讨论新的HasChanges属性。

属性HasChanges

在WCF RIA Services中,DomainContext类有一个名为HasChanges的属性,用于指示此上下文是否有任何待处理的更改。由于我们使用自跟踪实体进行客户端更改跟踪,因此将此新属性添加到每个实体类是合乎逻辑的,而且我们只需要在客户端添加它。在我们的示例应用程序中,此新属性由IssueVision.Data项目中的T4模板IssueVisionClientModel.tt生成。

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;

请注意,将此属性的初始值设置为true很重要。这是因为每当我们创建一个新的实体对象时,其初始State都设置为Added。由于处于Added状态的任何实体对象始终有待保存的更改,因此HasChanges的初始值应为true

接下来,让我们尝试弄清楚HasChanges属性需要更新到哪里。如前所述,ObjectChangeTracker类中有三个事件:ObjectStateChangingObjectStateChangedUpdateHasChanges。很容易得出结论,我们需要在ObjectStateChangedUpdateHasChanges事件触发时更新HasChanges

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
}

确定特定自跟踪实体对象是否有待处理更改的逻辑如下:首先,如果ChangeTrackerStateAdded,则实体对象有待处理的更改。其次,如果启用了更改跟踪,并且其State不是Unchanged,或者ObjectsAddedToCollectionPropertiesObjectsRemovedFromCollectionProperties中至少有一个不为空,则该实体对象也有待处理的更改。

ObjectStateChanged事件由OnObjectStateChanged()方法触发,并且此方法在AcceptChanges()RejectChanges()中都被调用,如上所述。UpdateHasChanges事件由OnUpdateHasChanges()方法触发,并且它在以下位置调用:

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

到目前为止,我们已经完成了关于这个新更改跟踪基础结构的讨论。接下来,我们将继续讨论如何使用这些新功能。

类IssueVisionModel的实现

让我们首先看一下IssueVisionModel类的构建方式。IssueVisionModel类实现了IssueVision.Common项目中的IIssueVisionModel接口,如果您熟悉之前使用WCF RIA Services构建的示例,您会注意到此接口类在这两个示例中非常相似。例如,它们都有SaveChangesAsync()RejectChanges()等方法,并且都有HasChangesIsBusy等属性。实际上,这是一件好事。这意味着即使这两个示例使用完全不同的数据访问层,模型类暴露的接口几乎相同,从而有可能重用视图和视图模型类中的大部分源代码。

在深入研究IssueVisionModel类之前,让我们先看一下IssueVision.WCFService项目,在那里我们可以找到所有服务引用。

项目IssueVision.WCFService

IssueVisionServiceClientPasswordResetServiceClient类都继承自基类ClientBase,该基类提供了创建可以调用服务的客户端对象的基础实现。对于这两个类中的每一个,我们都实现了单例模式,并添加了一个名为ActiveCallCount的新属性,该属性跟踪并发活动调用的数量。

#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"

由于单例设计模式和这个新的ActiveCallCount属性,我们现在可以实现IsBusy属性。

属性IsBusy

IsBusy定义如下:

/// <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);
  }
}

IsBusy的值在每次ActiveCallCount发生PropertyChanged事件时都会更新。如果ActiveCallCount不等于零,则将IsBusy设置为true;否则,将其设置为false

属性HasChanges

接下来,让我们检查一下如何实现HasChanges属性。基本上,如果模型类正在跟踪的任何自跟踪实体对象有任何待处理的更改,则此属性将设置为true。根据此示例应用程序的业务逻辑,我们将只更新Issue对象或User对象。因此,我们在IssueVisionModel类中添加了两个新属性:CurrentEditIssueCurrentEditUser。以下是CurrentEditUser属性的代码片段:

/// <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();
  }
}

如上面的代码所示,CurrentEditUser属性订阅了PropertyChanged事件,并且每当此自跟踪实体对象触发其HasChanges属性的PropertyChanged事件时,模型类上的HasChanges属性将通过ReCalculateHasChanges()方法重新计算。请不要混淆两个不同的HasChanges属性。一个定义在实体类级别,另一个定义在IssueVisionModel类内部。ReCalculateHasChanges()方法定义如下:

/// <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;
}

如果CurrentEditIssueHasChanges()CurrentEditUserHasChanges()函数返回true,则HasChanges属性设置为true。并且,CurrentEditIssueHasChanges()函数检查实体对象CurrentEditIssue是否有待处理的更改。这是通过检查对象本身是否有待处理的更改,以及遍历其所有导航属性(即PlatformAttributesFiles)来完成的。CurrentEditUserHasChanges()函数执行几乎相同的逻辑。

方法SaveChangesAsync()

既然我们知道了HasChanges属性的工作原理,我们就可以继续讨论SaveChangesAsync()方法了。

/// <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()首先检查是否有待处理的更改。如果属实,该方法将进一步验证CurrentEditIssue属性是否有待处理的更改。如果这也属实,则会实际调用UpdateIssueAsync(),并将对象_currentEditIssue传递进去。接下来,该方法对CurrentEditUser属性采取类似的方法。

在服务器端完成UpdateIssueAsync()UpdateUserAsync()之后,将调用_proxy_UpdateIssueCompleted()_proxy_UpdateUserCompleted()事件处理程序。

/// <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));
    }
  }
}

上面的UpdateIssueCompleted的事件处理程序首先检查服务器是否返回了任何错误或警告消息。如果一切正常,它将调用_currentEditIssue及其所有导航属性上的AcceptChanges(),这将把_currentEditIssueHasChanges属性设置回false。但是,如果出现问题,则不会调用AcceptChanges(),错误或警告消息将通过事件传递给任何ViewModel类。而且,如果用户选择取消任何失败的更新操作,将执行RejectChanges(),我们将在接下来的部分中讨论。

方法RejectChanges()

RejectChanges()相对容易理解,因为它在逻辑上与SaveChangesAsync()方法相似。

/// <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();
  }
}

模型类上的RejectChanges()方法将调用CurrentEditIssueCurrentEditUser上的RejectChanges()。而且,对于每个实体,RejectChanges()都会在对象本身及其所有是集合对象的导航属性上被调用。

在这里,我们完成了对IssueVisionModel模型类如何构建的讨论;我们将继续讨论实体属性的不同类别的新主题。

三种不同类别的实体属性

在WCF RIA Services中,我们可以通过使用元数据注释或部分类来调整某个实体属性的可访问性。以User类为例,它在EDM文件中定义如下:

基于此EF模型的自动生成的实体类,它直接反映了数据库中存在的内容。但是,使用WCF RIA Services,我们可以进一步调整此类,如下所示:

[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; }
  ......
}

上面的代码片段基本上告诉WCF RIA Services在客户端排除生成PasswordSaltPasswordHash等属性,并在User实体类中添加两个新属性PasswordNewPassword,并且还生成这两个属性在客户端。请注意,这两个新属性在我们的示例数据库中不存在。有了这种灵活性,我们可以将实体属性分为以下三类:

  1. 仅在客户端可用但在服务器端不可用的属性。
  2. 在客户端和服务器端都可用的属性,包括可以直接保存到数据库字段的属性,以及不可以的属性,例如上面的PasswordNewPassword
  3. 仅在服务器端可用且从未在客户端生成的属性,例如上面的PasswordSaltPasswordHash

遗憾的是,如果我们选择自跟踪实体和WCF服务作为我们的数据访问层,那么上面看到的许多这些出色的功能将不可用,我们必须以一种略有不同的方式进行操作。

仅在客户端可用的属性

让我们先从一个简单的案例开始。对于仅在客户端可用的属性,我们可以采用与WCF RIA Services相同的方法:使用部分类在客户端添加新属性。

/// <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;
  ......
}

在客户端和服务器端都可用的属性

处理在客户端和服务器端都可用的属性似乎很简单,直到我们需要排除PasswordHash等属性并添加NewPassword等新属性,以便它们在客户端和服务器端都可用,即使不存在这样的数据库字段。

我们的方法是使用Entity Framework的一个高级功能,称为“虚拟表”。如果您不熟悉,这里有一个指向MSDN文档的链接。接下来,我们将逐步介绍如何将User添加为虚拟表到我们的EDM文件中。

  1. 首先,我们需要使用XML编辑器打开“IssueVision.edmx”文件,方法是右键单击文件,选择“打开方式”,然后选择“XML编辑器”,然后单击“确定”。

  2. 在SSDL部分添加一个EntitySetDefiningQuery元素,如下所示:
    <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. 在定义EntityType项的EntityType部分(在SSDL部分内)添加一个EntityType
    <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. 在CSDL部分添加一个EntitySet元素,如下所示:
    <EntitySet Name="Users" EntityType="IssueVisionModel.User" />
  5. 在定义EntityType项的EntityType部分(在CSDL部分内)添加一个EntityType
    <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. 保存更改并切换回设计器,以便我们可以将实体映射到我们刚刚创建的虚拟表,它应该看起来像这样:

  7. 接下来,我们将向SSDL部分添加三个自定义函数,用于我们新创建的User实体的插入/删除/更新操作。如果您需要有关如何定义存储模型中的自定义函数的更多信息,这里有一个指向MSDN文档的链接。以下是三个自定义函数之一:
    <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. 再次,在添加这三个自定义函数后保存更改,并切换回设计器,以便我们可以映射实体和自定义函数。

使用虚拟表的优点是实体属性不一定都是数据库字段,甚至实体类本身也不必基于一个数据库表。它可以是多个表的连接。此外,没有必要定义匹配的插入/删除/更新自定义函数。就像我们示例应用程序中的PasswordResetUser实体类一样,它基本上成为一个只读视图。

最后,使用虚拟表的一个注意事项是,这些内容实际上并不存在于我们的示例数据库中。如果您运行“更新模型向导”,Entity Framework 4.0 Designer将删除SSDL的所有自定义。因此,如果您需要更新EF模型,请保留这些更改的副本。

仅在服务器端可用的属性

接下来,我们将讨论如何实现三种实体属性类别的最后一个。在我们的示例应用程序中,仅在服务器端可用的属性是PasswordSaltPasswordHashPasswordAnswerSaltPasswordAnswerHash。很明显,这四个属性应该保留在服务器端,因为将它们传输到客户端可能会导致安全泄露。一种将属性保留在服务器端的方法是将它们从EF模型的UserPasswordResetUser实体类中移除,并使用函数导入来检索和更新这些数据库字段。这里有一个MSDN文档的链接,如果您不熟悉如何创建函数导入。

首先,使用XML编辑器打开“IssueVision.edmx”文件,并将所有相关的自定义函数添加到SSDL部分。以下只是其中之一:

<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>

之后,使用“添加函数导入”对话框添加函数导入,如下所示:

我们需要添加总共六个函数导入,如下面的“模型浏览器”所示:

让我们看看如何使用这些新的函数导入来检索和更新仅服务器端可用的属性。例如,context.GetPasswordAnswerHash(user.Name).First()将根据特定的用户名从User表中检索PasswordAnswerHash字段。而且,我们可以使用context.ExecuteFunction来调用UpdatePasswordHashAndSalt,并传递密码盐和密码哈希值作为参数。PasswordResetService类中的ResetPassword()方法演示了它们是如何被使用的。

/// <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);
  }
}

使用函数导入检索和更新仅服务器端可用的属性可以确保这些属性不会在客户端公开。我能想到一个问题是,如果有很多仅服务器端可用的属性,这种方法可能无法很好地扩展。但是,对于大多数LOB应用程序,仅服务器端属性的数量应该相对较少,而大多数属性应该在客户端和服务器端都可用。因此,这里不应该有太大的可扩展性问题。

服务器端更新逻辑

在我们完成本文之前,最后一个主题是我们如何在服务器端实际执行添加/删除/更新操作。以下是IssueVisionService类中的UpdateIssue()方法是如何实现的。

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;
      }
    }
  }
}

如上面的方法所示,我们根据ChangeTrackerState属性确定是添加、删除还是更新问题实体。如果StateAdded,我们将添加一个新的问题。如果StateDeleted,我们应该从数据库中删除该问题。如果StateUnchangedModified,我们将执行更新操作。此外,无论它是添加、删除还是更新操作,我们都通过简单地调用context.Issues.ApplyChanges(issue)然后调用context.SaveChanges()来保存更改。

下一步

我们在第二篇文章中涵盖了很多主题。在下一部分中,我们将继续讨论使用自跟踪实体的客户端和服务器端验证逻辑。希望您发现这篇文章很有用,请在下方评分和/或留下反馈。谢谢!

历史

  • 2011年1月 - 初版。
  • 2011年3月 - 更新以修复多个错误,包括内存泄漏问题。
© . All rights reserved.