所有可编辑的业务对象都应该对该对象是否被创建,其中的数据是否被修改,或是对象是否被标记为已删除这样状态保持跟踪。使用验证规则功能,对象还可以保持对有效性的跟踪。表3-4列出了BusinessBase中关于对象状态的属性。
表3-4 对象状态属性
属性 |
描述 |
IsNew |
表明对象在内存中的主识别值是否与数据库的主键对应——如果不对应,则该对象就是一个新对象 |
IsDirty |
表明对象在内存中的数据是否与数据库中的数据不一致——如果不一致,则该对象就是一个脏对象 |
IsValid |
表明对象当前是否有任何失效的验证规则——如果有,则该对象就是无效的 |
IsSavable |
结合了IsValid和IsDirty |
IsDeleted |
表明对象是否被标记为已删除 |
我现在来讨论新对象、脏对象、有效对象和标记删除对象背后的概念。
如果某个对象是“新”的,这就是说该对象存在于内存中,但是没有存在于数据库或其他持久存储中。如果该对象的数据存在于数据库中,那么这个对象就被认为是“旧”的。通常我会这样思考这个问题:如果对象中的主键值能够对应数据库中已经存在的某个主键值,则该对象就是旧的;否则的话它就是一个新对象。
IsNew属性的值存储在_isNew域中。当首次创建一个对象的时候,这个值默认该对象是个新对象:
Private bool _isNew = true;
如果该对象随后装载了来自数据库的数据,那么_isNew域就通过一个保护的MarkOld()方法被设成了false:
protected virtual void MarkOld()
{
_isNew = false;
MarkClean();
}
请注意这个过程还把该对象设置成了“干净”的状态——在后面讨论IsDirty属性的时候会讲到这个概念。如果某个对象刚刚装载了从数据库提取出来的数据,我们就可以安全地假设它的数据与数据库中的数据是一致的,数据是没有修改过的——因此是“干净”的。
还有一个相关的MarkNew()方法:
protected virtual void MarkNew()
{
_isNew = true;
_isDeleted = false;
MarkDirty();
}
通常来讲,这个方法会在某个现存的对象被删除的时候被调用,但是它可以在任何业务开发人员知道该对象并不对应数据库中的任何数据的时候被使用。在这种情况下,这个对象不止是“新”的,而且还必须是“脏”的,因为该对象中的数据与数据库并不一致。标记删除的概念将会在后面谈到IsDeleted属性的时候讨论,但是一个新的对象不应该被标记成删除的,所以这个标记是设置成false的。
在第4章中实现的数据访问代码需要知道某个对象是新的还是旧的。IsNew属性将会控制选择是在数据库中插入数据还是更新数据。
有的时候,IsNew属性对UI开发人员也是非常有用的。有些UI对于一个新对象和一个已经存在的对象会有不同的行为。编辑对象主键数据的能力就是一个很好的例子——对象经常只有在其数据已经被存进数据库之后才是可以编辑的。当对象变“旧”以后,主键就确定了。
当对象域中的值与数据库不一致的时候,该对象被认为是“脏”的或是已经改变了的。如果对象域中的值与数据库一致,则该对象就不是“脏”的。我们实际上是无法总是知道对象的值是不是与数据库一致的,所以这里的实现采取了一种“最佳猜测”的方法。这种实现需要业务开发人员在对象已经被改变,并因此变脏的时候将其表现出来。
值的当前状态是在一个域中维护的:
Private bool _isDirty = true;
然后这个值被作为一个属性暴露出来:
[Browsable(false)]
public virtual bool IsDirty
{
get { return _isDirty; }
}
请注意这个属性被标记成了是virtual的。这很重要,因为有的时候业务对象变脏的原因并不是简单地因为它的数据被修改了。例如,考虑一下一个包含着子对象集合的业务对象——即使业务对象的数据没有发生变化,如果任何它的子对象发生了变化,它还是会因此而变脏。在这种情况下,业务开发人员就需要重载并实现一个更复杂的IsDirty属性。在第7章的示例业务对象的实现中会有对此清晰的描述。
还要请你注意的是,这个属性是被标记为System.ComponentModel名字空间中的[Browsable()]的。[Browsable()]告诉数据绑定不要自动地绑定这个属性。如果没有[Browsable()],数据绑定就会自动地在表格和窗体上显示这个属性——通常这个属性是不会被显示出来的。[Browsable()]也会被使用在BusinessBase中的其他属性上。
IsDirty属性的默认值是true,因为一个新对象的域值是不会与数据库一致的。如果对象最终装载了数据库中的值,这个值就会在MarkOld()被调用的时候被改为false。请记住,MarkOld()会调用MarkClean()方法:
protected void MarkClean()
{
_isDirty = false;
OnUnknownPropertyChanged();
}
这个方法不仅会将值设置成false,而且还会调用在Csla.Core.BindableBase中实现的OnUnknownPropertyChanged()方法,来为所有的对象属性触发PropertyChanged事件。这将会通知数据绑定说该对象已经改变了,以便Windows Forms可以对显示给用户的内容进行刷新。
这里还有一个相关的MarkDirty()方法。对象在任何时候都可以调用这个方法,包括任何属性值改变的时候,或当调用MarkNew()方法的时候。
当属性值发生变化的时候,一个特定的PropertyChanged事件将会为这个属性所触发。
如果MarkDirty()在其他时候被调用,就是说在特定属性值没有发生变化的情况下,那么PropertyChanged事件将会为所有的对象属性所触发。这样,如果有任何对象的属性与UI控件绑定的话,数据绑定就会被告知任何变化。
说得清楚一点,这样做的目的就是保证在任何对象状态发生变化的时候都至少会有一个PropertyChanged事件被触发。如果某个特定的属性被改变了,那么PropertyChanged事件就应该只为那个属性触发。但是如果没有办法来判断是哪个属性被改变了(就像当对象被持久化到数据库中的时候),我们就别无选择,只能为每个属性都触发PropertyChanged事件。
这个功能的实现需要将MarkDirty()方法重载几次:
protected void MarkDirty()
{
MarkDirty(false);
}
[EditorBrowsable(EditorBrowsableState.Advanced)]
protected void MarkDirty(bool suppressEvent)
{
_isDirty = true;
if (!suppressEvent)
OnUnknownPropertyChanged();
}
第一个重载可以被业务开发人员用来手工的将对象标记为改变了的。这个重载是为了在不知道属性是否发生变化的情况下使用的。
第二个重载是给PropertyHasChanged()方法来调用的:
protected virtual void PropertyHasChanged(string propertyName)
{
ValidationRules.CheckRules(propertyName);
MarkDirty(true);
OnPropertyChanged(propertyName);
}
PropertyHasChanged()方法是被业务开发人员用来指明某个特定的属性已经发生变化了的。请注意在这种情况下,该属性的所有验证规则都会被检查一遍(本章稍后会谈到这部分的细节)。然后该对象就通过为特定发生变化的属性来触发PropertyChanged事件而被标记成是脏的了。
提示:这个方法是virtual的,这就允许你在需要的时候在处理中添加额外的步骤。此外,这还意味着你在需要的时候可以覆盖这个行为来实现域级别的脏数据跟踪。
通过将属性名字作为字符串传递来调用PropertyHasChanged()方法意味着将属性名字硬编码在代码中了。众所周知,字符串文本非常难以维护,所以有一个重载自动地在运行时收集属性的名字:
[System.Runtime.CompilerServices.MethodImpl(
System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
protected void PropertyHasChanged()
{
string propertyName =
new System.Diagnostics.StackTrace().
GetFrame(1).GetMethod().Name.Substring(4);
PropertyHasChanged(propertyName);
}
这种实现使用了System.Diagnostics来提取调用PropertyHasChanged()的方法或属性的名字。[MethodImpl()]特性使编译器不要把这段代码直接与属性本身合并在一起,因为那样会使System.Diagnostics的调用变得混乱。
这样调用System.Diagnostics会造成一个性能问题(与使用反射类似),但是我通常愿意用这样的代价来避免因为属性名字来在业务类中使用字符串文本。使用这个方法,业务对象的属性是这个样子的:
public string Name
{
get
{
CanReadProperty(true);
return _name;
}
set
{
CanWriteProperty(true);
if (_name != value)
{
_name = value;
PropertyHasChanged();
}
}
}
PropertyHasChanged()的调用不需要属性的名字,因为它可以自动地使用System.Diagnostics来提取。如果你觉得这样做对于性能的代价太大,你也可以将属性名字作为每个PropertyHasChanged()方法调用的参数进行硬编码。
不管用什么方法,我们都要检查属性的验证规则,IsDirty属性被设为true,并且触发了合适的PropertyChanged事件。
如果一个对象当前没有任何失效的验证规则,则这个对象就被认为是有效的。在本章后面将会被涉及的Csla.Validation名字空间提供了对业务规则的管理。IsValid属性仅仅暴露了表示该对象是否含有已经失效了的规则的一个标志位:
[Browsable(false)]
public virtual bool IsValid
{
get { return ValidationRules.IsValid; }
}
与IsDirty一样,这个属性被标记成是[Browsable()]的,这样数据绑定就会默认忽略这个属性。
一个对象应该只有在有效而且其中的数据已经被修改的情况下,才能被保存到数据库中。IsValid属性可以显示某个对象是不是有效的,而IsDirty属性可以显示该对象的数据是否被修改
过。IsSavable属性简单地将这两种属性合并在了一起:
[Browsable(false)]
public virtual bool IsSavable
{
get { return (IsDirty && IsValid); }
}
这个属性主要的目的就是允许Windows Forms的UI开发人员来绑定保存按钮的Enabled属性,这样该按钮只有在对象能够被保存的时候才是可用的。
CSLA.NET框架允许对象被延迟地删除或者是立即地删除。立即删除的方法直接将对象数据从数据库中删除,无须首先将对象装载到内存中。这样做需要事先知道该对象的主键值,因为与数据访问直接相关,这部分将在第4章中讨论。
延迟删除的方法需要将对象装载到内存。然后用户就可以查看并操作对象的数据,并决定是否删除该对象,在这种情况对象是被标记删除的。该对象并没有在当时被立即地删除掉,而是在保存回数据库的时候被删除的。这里,保存数据库的动作并没有插入或是更新该对象的数据,而是将该对象从数据库中删除。
这个方法对于集合中的子对象尤其有用。在这种情况下,用户可能会在添加和更新某些子对象的同时删除另外一些子对象。所有的插入、更新和删除操作作为一个批处理在集合被保存到数据库的时候统一处理。
对象是否被标记为删除了的由_isDeleted域来跟踪,并通过一个IsDeleted属性暴露出来。与IsDirty一样,也有一个protected的方法来在必要的时候将该对象标记成删除了的:
protected void MarkDeleted()
{
_isDeleted = true;
MarkDirty();
}
当然,将对象标记成删除了的这个动作本身也修改了对象的数据,所以还要调用MarkDirty()方法来表明该对象的状态已经被修改了。
MarkDeleted()方法是由Delete()和DeleteChild()方法来调用的。Delete()方法是用来将一个非子对象标记为延迟删除的,而DeleteChild()方法是为父对象(如集合)用来将一个子对象标记为延迟删除的:
public void Delete()
{
if (this.IsChild)
throw new NotSupportedException(Resources.ChildDeleteException);
MarkDeleted();
}
internal void DeleteChild()
{
if (!this.IsChild)
throw new NotSupportedException(Resources.NoDeleteRootException);
MarkDeleted();
}
这两个方法所作的事情是一样的:调用MarkDelete()。但是Delete()的作用域是public的,并且只能用于非子对象(后面将会对父对象和子对象的行为加以讨论)。相反的,DeleteChild()只能用于子对象。因为该方法是为了BusinessListBase使用的,所以它的作用域是internal的。