使用 Self-Tracking Entities 和 WCF 服务构建的 Silverlight 示例 - 第 2 部分
本系列文章的第二部分,介绍使用自跟踪实体、WCF服务、WIF、MVVM Light Toolkit、MEF和T4模板创建Silverlight业务应用程序。
- 请访问此 项目站点 获取最新版本和源代码。
文章系列
本文是关于使用自跟踪实体、WCF服务、WIF、MVVM Light Toolkit、MEF和T4模板开发Silverlight业务应用程序的系列文章的第二部分。
- 第一部分 - 简介、系统要求和体系结构概述
- 第二部分 - 使用AcceptChanges、RejectChanges和HasChanges进行更改跟踪
- 第三部分 - 带有验证逻辑的增强型自跟踪实体
- 第四部分 - 使用WIF和其他主题进行身份验证和授权
目录
引言
在第二部分中,我们将介绍如何使用自跟踪实体实现客户端更改跟踪。在VS2010的ADO.NET自跟踪实体生成器的当前版本中,已经有一个名为AcceptChanges()
的方法,但是没有实现RejectChanges()
方法或HasChanges
属性。我们将探讨如何在我们的增强型自跟踪实体生成器版本中添加这些功能。之后,我们还将介绍此示例的其他部分如何与这个新的数据访问层无缝协同工作的几个主题。
背景
让我们首先检查一下ADO.NET自跟踪实体生成器(来自VS2010)自动生成的实体类。基本上,每个实体类都是一个POCO(纯粹的CLR对象),并附带两个附加接口:IObjectWithChangeTracker
和INotifyPropertyChanged
。
这里感兴趣的接口是IObjectWithChangeTracker
,它为每个自动生成的实体类添加了一个新的ChangeTracker
属性。此属性存储给定对象的子图的所有更改跟踪信息,其类型为ObjectChangeTracker
。
ObjectChangeTracker
是我们找到大部分客户端更改跟踪逻辑的类。让我们简要回顾一下我们以后可能会用到的一些现有方法和属性。
ChangeTrackingEnabled
顾名思义,它存储一个布尔值,指示此自跟踪实体对象是否启用更改跟踪。State
存储Unchanged
、Added
、Modified
或Deleted
之一的值,用于跟踪自跟踪实体对象的不同状态。OriginalValues
存储已更改属性的原始值。ObjectsAddedToCollectionProperties
存储已更改的集合属性中添加的对象。ObjectsRemovedFromCollectionProperties
存储已更改的集合属性中删除的对象。
除了上面提到的属性之外,ObjectChangeTracker
类还实现了AcceptChanges()
方法,我们将在后面讨论。
更改跟踪基础结构
在简要概述了ObjectChangeTracker
类之后,我们现在可以开始讨论我们将要为完整的客户端更改跟踪基础结构添加哪些附加逻辑。让我们先讨论ObjectChangeTracker
类中的现有和新事件。
事件ObjectStateChanging、ObjectStateChanged和UpdateHasChanges
我们的新ObjectChangeTracker
类定义了三个事件:ObjectStateChanging
、ObjectStateChanged
和UpdateHasChanges
。ObjectStateChanging
事件存在于原始版本中,而另外两个是新增的。
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
属性以及ObjectsAddedToCollectionProperties
和ObjectsRemovedFromCollectionProperties
属性。然后将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
中存储的所有原始值,然后复制ObjectsAddedToCollectionProperties
和ObjectsRemovedFromCollectionProperties
,最后使用副本回滚所有这些值。与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
类中有三个事件:ObjectStateChanging
、ObjectStateChanged
和UpdateHasChanges
。很容易得出结论,我们需要在ObjectStateChanged
或UpdateHasChanges
事件触发时更新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
}
确定特定自跟踪实体对象是否有待处理更改的逻辑如下:首先,如果ChangeTracker
的State
是Added
,则实体对象有待处理的更改。其次,如果启用了更改跟踪,并且其State
不是Unchanged
,或者ObjectsAddedToCollectionProperties
或ObjectsRemovedFromCollectionProperties
中至少有一个不为空,则该实体对象也有待处理的更改。
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()
等方法,并且都有HasChanges
、IsBusy
等属性。实际上,这是一件好事。这意味着即使这两个示例使用完全不同的数据访问层,模型类暴露的接口几乎相同,从而有可能重用视图和视图模型类中的大部分源代码。
在深入研究IssueVisionModel
类之前,让我们先看一下IssueVision.WCFService项目,在那里我们可以找到所有服务引用。
项目IssueVision.WCFService
IssueVisionServiceClient
和PasswordResetServiceClient
类都继承自基类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
类中添加了两个新属性:CurrentEditIssue
和CurrentEditUser
。以下是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
是否有待处理的更改。这是通过检查对象本身是否有待处理的更改,以及遍历其所有导航属性(即Platform
、Attributes
和Files
)来完成的。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()
,这将把_currentEditIssue
的HasChanges
属性设置回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()
方法将调用CurrentEditIssue
和CurrentEditUser
上的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在客户端排除生成PasswordSalt
和PasswordHash
等属性,并在User
实体类中添加两个新属性Password
和NewPassword
,并且还生成这两个属性在客户端。请注意,这两个新属性在我们的示例数据库中不存在。有了这种灵活性,我们可以将实体属性分为以下三类:
- 仅在客户端可用但在服务器端不可用的属性。
- 在客户端和服务器端都可用的属性,包括可以直接保存到数据库字段的属性,以及不可以的属性,例如上面的
Password
和NewPassword
。 - 仅在服务器端可用且从未在客户端生成的属性,例如上面的
PasswordSalt
和PasswordHash
。
遗憾的是,如果我们选择自跟踪实体和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文件中。
- 首先,我们需要使用XML编辑器打开“IssueVision.edmx”文件,方法是右键单击文件,选择“打开方式”,然后选择“XML编辑器”,然后单击“确定”。
- 在SSDL部分添加一个
EntitySet
和DefiningQuery
元素,如下所示:<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>
- 在定义
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>
- 在CSDL部分添加一个
EntitySet
元素,如下所示:<EntitySet Name="Users" EntityType="IssueVisionModel.User" />
- 在定义
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>
- 保存更改并切换回设计器,以便我们可以将实体映射到我们刚刚创建的虚拟表,它应该看起来像这样:
- 接下来,我们将向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>
- 再次,在添加这三个自定义函数后保存更改,并切换回设计器,以便我们可以映射实体和自定义函数。
使用虚拟表的优点是实体属性不一定都是数据库字段,甚至实体类本身也不必基于一个数据库表。它可以是多个表的连接。此外,没有必要定义匹配的插入/删除/更新自定义函数。就像我们示例应用程序中的PasswordResetUser
实体类一样,它基本上成为一个只读视图。
最后,使用虚拟表的一个注意事项是,这些内容实际上并不存在于我们的示例数据库中。如果您运行“更新模型向导”,Entity Framework 4.0 Designer将删除SSDL的所有自定义。因此,如果您需要更新EF模型,请保留这些更改的副本。
仅在服务器端可用的属性
接下来,我们将讨论如何实现三种实体属性类别的最后一个。在我们的示例应用程序中,仅在服务器端可用的属性是PasswordSalt
、PasswordHash
、PasswordAnswerSalt
和PasswordAnswerHash
。很明显,这四个属性应该保留在服务器端,因为将它们传输到客户端可能会导致安全泄露。一种将属性保留在服务器端的方法是将它们从EF模型的User
和PasswordResetUser
实体类中移除,并使用函数导入来检索和更新这些数据库字段。这里有一个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;
}
}
}
}
如上面的方法所示,我们根据ChangeTracker
的State
属性确定是添加、删除还是更新问题实体。如果State
是Added
,我们将添加一个新的问题。如果State
是Deleted
,我们应该从数据库中删除该问题。如果State
是Unchanged
或Modified
,我们将执行更新操作。此外,无论它是添加、删除还是更新操作,我们都通过简单地调用context.Issues.ApplyChanges(issue)
然后调用context.SaveChanges()
来保存更改。
下一步
我们在第二篇文章中涵盖了很多主题。在下一部分中,我们将继续讨论使用自跟踪实体的客户端和服务器端验证逻辑。希望您发现这篇文章很有用,请在下方评分和/或留下反馈。谢谢!
历史
- 2011年1月 - 初版。
- 2011年3月 - 更新以修复多个错误,包括内存泄漏问题。