在 C# 中实现 PropertyBag






4.86/5 (22投票s)
2005年11月25日
14分钟阅读

213909

4235
设计和实现 C# 中的 PropertyBag。
引言
本文详细介绍了 C# 中 PropertyBag
类的设计、编码和文档。
在设计类时,会赋予它们设计者设想的客户端所需的属性。例如,一个 Address
类可能需要 PostCode
和 StreetName
属性,这些属性会被设计到类中。
类经常会在设计者意图范围之外被使用。例如,我们假设的 Address
类开始在美国地区使用,而设计者并未设想有非英国的客户。因此,该类现在需要一个 ZipCode
属性。
一方面,可以扩展 Address
类的代码,重新编译程序集,并重新分发。另一方面,如果 Address
类公开了一个 PropertyBag
对象,那么该类的使用者可以为 PropertyBag
分配临时属性,从而避免了成本高昂的编码、编译和分发。
PropertyBag 类的要求
获取和设置属性
PropertyBag
的主要要求是使客户端类能够添加新属性,为这些属性分配和重新分配值,并读取这些属性的当前值。
易用性
一个次要功能是设计和实现该类,使其接口非常直观且易于使用。为了实现这一点,在访问 Property
类时,PropertyBag
类使用了索引器。
考虑过类型安全的集合,但决定避免过度复杂化代码;类型安全的集合将是后续文章的主题。
使用了内部类来简化 PropertyBag
接口,这将在本文中进一步演示。
直观地处理现有属性
另一个功能是考虑包含 PropertyBag
的类的“真实”属性。
例如,如果父类是 Address
类(它已经描述了一个名为 Line1
的真实属性),并且 Address
类的客户端尝试向 Address
类的 PropertyBag
添加一个 PostCode
属性,那么 PropertyBag
的 PostCode
属性将指向 Address
类中的“真实”属性,而不是 PropertyBag
中具有重复名称的临时属性。
在 PropertyBag
类中使用反射将能够实现此要求。
在单个属性上实现更新和已更新事件
PropertyBag
类中的属性需要实现事件,以便当 PropertyBag
中的特定属性被更新时,事件订阅者能够收到通知。要实现的事件是 Updating
和 Updated
。Updating
事件将包括一个机制,用于向生成事件的属性信号回该更新将被取消,从而使事件处理程序能够验证拟议的新值,并在验证失败时拒绝新值。
本文假定读者对 C# 中事件和委托的实现有很好的理解。这个主题在文章 Using Events and Delegates in C# 中进行了介绍。
例如,如果 Address
类中的 PropertyBag
可以被客户端用来添加一个新的 ZipCode
属性。添加 ZipCode
属性的类可以订阅 Updating
事件,并拒绝所有非数字的新值。
可序列化性
PropertyBag
和 Property
对象需要是可序列化的,这样,只要所有者对象(例如本例中的 Address
类)是可序列化的,那么 PropertyBag
和 Property
对象也将被序列化。
线程安全
最后一个要求是 PropertyBag
类必须是线程安全的。实现事件会迫使设计者思考这些事件可能被消耗的场景。事件处理程序在单线程应用程序中有用,但在多线程应用程序中通常更有用,其中一个线程中的属性值更改会在另一个线程中引发事件(例如 Windows Explorer 中的树视图和列表视图)。
PropertyBag
实现中对属性的写入以及对可写入属性的读取必须实现锁定以确保线程安全。
结论
PropertyBag
类是一个有用的实用工具,可以包含在通用类中,以支持在设计后引入临时属性。它的实现将实际演示索引器、内部类、反射、事件和线程安全。
从最后开始 - 消费 PropertyBag 类
基本用法
对 PropertyBag
的最佳介绍是让客户端在使用中看到它。示例代码使用了 Address
类,该类具有一些自己的属性,并聚合了一个 PropertyBag
实例。
这是一个消费者(控制台应用程序中的 Main
)的代码片段,以最基本的形式使用 PropertyBag
创建一个新属性,并更新该属性的值。
/// Create an instance of the Address object
WebSoft.Address objAddress = new Address();
/// Set the instances in-built properties
objAddress.Line1 = "21 North Street";
objAddress.Line2 = "Northby";
objAddress.Line3 = "";
objAddress.City = "Northtown";
objAddress.County = "Northshire";
objAddress.Country = "Northaria";
objAddress.PostCode = "NN1 1NN";
/// Set the value of the ZipCode property to 123456
/// The ZipCode property is created, as it has not
/// previously been referenced
objAddress.Properties["ZipCode"].Value = 123456;
/// Update the value of the ZipCode property from 123456
/// to 789012. The existing property is referenced, and
/// changed
objAddress.Properties["ZipCode"].Value = 789012;
/// Get the value of the ZipCode property, and write
/// to the console
Console.WriteLine("ZipCode: {0}",
objAddress.Properties["ZipCode"].Value);
示例代码很简单。Address
类具有内置的 Line1
、Line2
、Line3
、City
、County
、Country
和 PostCode
属性。它还有一个 Properties
属性,该属性公开了一个 PropertyBag
类的实例。
开发人员需要向该类添加一个 ZipCode
属性,并通过操作 PropertyBag
实例来实现,而无需重新编码。
第一次简单地使用索引设置 Value
会导致将一个新属性添加到 bag 中。后续使用相同的索引将导致 Value
被更新。
将“真实”属性添加到 PropertyBag
当开发人员决定向属性包添加一个名为 Line1
的属性时会发生什么?
拥有两个不同的 Line1
属性,一个由 objAdress.Line1
索引,另一个由 objAddress.Properties["Line1"].Value
索引,这将导致数据重复。
以下代码片段演示了 PropertyBag
类如何处理这种情况。
/// Change the value of the Line1 property in the PropertyBag
/// (and hence the underlying value of the Address objects
/// "real" Line1 property).
objAddress.Properties["Line1"].Value = "22 North Street";
/// Demonstrate that the "real" Line1 property has been
/// updated by writing its current value to the console
Console.WriteLine("Line1: {0}", objAddress.Line1);
当 PropertyBag
被要求使用“Line1
”进行索引时,它会使用反射来确定 Address
类是否已有一个名为 Line1
的“真实”属性,并设置真实属性的值。
此后,使用“Line1
”索引在 PropertyBag
中设置或获取值将设置为底层的 objAddress.Line1
属性,而不是 PropertyBag
中的重复项。
序列化
标准的 .NET Stream
和 Formatter
对象可用于序列化和反序列化容器/所有者对象(即 Address
类的实例),包括其聚合的 PropertyBag
和 Property
对象实例,前提是所有者对象本身是可序列化的。
调用序列化和反序列化的代码如下。
/// Create a new file called MyAddress.xml to hold the xml
/// representation of the serialized address object
Stream objFileStreamWrite = File.Create("MyAddress.xml");
/// Create a Soap Formatter such that the serialized object
/// will be in Simple Object Access Protocol format
SoapFormatter objSoapFormatter = new SoapFormatter();
/// It is vital to unwire the events in the Properties,
/// as the events refer to event handlers in the
/// test harness which cannot be deserialized. This step
/// is critical.
objAddress.Properties["Line1"].Updated -=
new UpdatedEventHandler(Property_Updated);
objAddress.Properties["ZipCode"].Updating -=
new UpdatingEventHandler(ZipCode_Updating);
objAddress.Properties["ZipCode"].Updated -=
new UpdatedEventHandler(Property_Updated);
/// Serialize the address object, including the property
/// bag into the file, and close the file
objSoapFormatter.Serialize(objFileStreamWrite, objAddress);
objFileStreamWrite.Close();
/// Open the recently (very) saved file containing the xml
/// representation of the object
Stream objFileStreamRead = File.OpenRead("MyAddress.xml");
/// Create an instance of the deserialized address (not yet
/// constructed)
WebSoft.Address objAddressDeserialized;
/// Instantiate the new address object, including the property
/// bag values, from the xml file, and close the file
objAddressDeserialized =
(Address)(objSoapFormatter.Deserialize(objFileStreamRead));
objFileStreamRead.Close();
/// Prove that the serialization / deserialization has worked by
/// sending a "real" property value, and
/// a "real" property value reflected by the bag, and a genuine
/// property bag property value to the console
Console.WriteLine(objAddressDeserialized.Line1);
Console.WriteLine(objAddressDeserialized.Properties["Line1"].Value);
Console.WriteLine(objAddressDeserialized.Properties["ZipCode"].Value);
使用属性的单个 Updated 事件
bag 中的每个单独属性都有自己的 Updated
事件,该事件在属性值更改后立即触发。这包括 bag 中的“真实”属性。
可以按如下方式订阅属性的 Updated
事件。
/// Subscribe to the Updating event of the
/// ZipCode property within the Address objects PropertyBag
objAddress.Properties["ZipCode"].Updating +=
new UpdatingEventHandler(ZipCode_Updating);
然后可以按如下方式编写一个事件处理程序来处理该事件。
/// <summary> /// Event handler for the Line1 and ZipCode properties Updated event /// </summary> /// <param name="sender">The instance of the Property class /// that raised the event</param> /// <param name="e">An instance of the UpdatedEventArgs class</param> private static void Property_Updated(object sender, UpdatedEventArgs e) { /// Write a message to the console string strMsg = "Property {0} changed from {1} to {2}"; Console.WriteLine(strMsg, e.Name, e.OldValue, e.NewValue); }
每当 ZipCode
属性更新时,Property_Updated
将在更改后立即触发。这在有多个视图查看同一模型的情况下可能很有用。例如,Windows Explorer 包含一个左侧的树视图和一个右侧的列表视图。如果一个文件夹粘贴到列表视图中,树视图需要知道它,以便它的视图可以被更新。
有一个警告是,如果某个“真实”属性有一个或多个 Updated
事件的订阅者,并且客户端类直接使用“真实”属性,则事件不会被引发。
例如,进行此事件订阅后。
/// Subscribe to the Updated event of the Line1 property in
/// the Address objects PropertyBag
/// (which corresponds to a "real"
/// Line1 property within the Address object
objAddress.Properties["Line1"].Updated +=
new UpdatedEventHandler(Property_Updated);
此代码。
objAddress.Properties["Line1"].Value = "22 North Street";
将引发事件,但此代码。
objAddress.Line1 = "23 North Street";
将不会。
使用属性的单个 Updating 事件
Updating
事件与 Updated
事件有一个重要的区别(除了它在更改之前触发,而不是之后触发!)。那就是 UpdatingEventArgs
参数有一个 Cancel
属性。如果至少一个 Updating
事件将 Cancel
属性设置为 true
,则更新将被取消。
这意味着 Updating
事件处理程序是构建 bag 中属性验证的理想场所,以便如果违反了验证规则,更新将被拒绝。
进行此事件订阅后。
/// Subscribe to the Updating event of the ZipCode
/// property within the Address objects PropertyBag
objAddress.Properties["ZipCode"].Updating +=
new UpdatingEventHandler(ZipCode_Updating);
可以编写一个事件处理程序,该处理程序将确保邮政编码是数字的。如果任何客户端尝试将邮政编码设置为非数字值,更新将被拒绝,并且值将保持不变。
/// <summary>
/// Event handler for the ZipCode properties Updating event
/// </summary>
/// <param name="sender">The instance of the Property class
/// that raised the event</param>
/// <param name="e">An instance of the UpdatingEventArgs class</param>
private static void ZipCode_Updating(object sender, UpdatingEventArgs e)
{
string strMsg;
try
{
/// Ascertain that the proposed new value is numeric
int i = int.Parse(e.NewValue.ToString());
strMsg = "Property {0} about to change from {1} to {2}";
}
catch (System.FormatException)
{
/// Ascertain that the proposed new value is not numeric,
/// therefore set the Cancel property of the event args to true,
/// thereby preventing the Value of the Property from being set
/// to the new value
e.Cancel = true;
strMsg =
"Property {0} not changed from {1} to {2}: Zip codes are numeric";
}
/// Write the message to the console
Console.WriteLine(strMsg, e.Name, e.OldValue, e.NewValue);
}
同一 Updating
事件可能有多个订阅者(即,它是一个多播委托)。在这种情况下,如果任何单个订阅者将 e.Cancel = true;
设置为 true
,则更新将被取消,而不管其他订阅者做什么。
完成“从最后开始”
希望 PropertyBag
类的功能足以让您下载它并开始在您的类中使用它。当然,一旦您了解了主要功能以及如何调用它们,您就可以直奔主题。如果您想一窥内部情况,并了解 PropertyBag
类是如何构建的,请继续阅读。
设计 PropertyBag
在急于开发 PropertyBag
之前,它使用了统一建模语言(UML)进行设计。
遵循了敏捷的设计方法,即设计必须足够好,以确保后续的开发都经过了深思熟虑和记录。设计包括两个序列图和一个类图。作为敏捷开发,设计文档是手绘的,因为电子版本的设计会花费额外的时间而不会为交付物增加任何价值。
然而,设计范围包括向本文的读者传达理解,因此如果任何读者因手写或整体的粗糙而无法解读设计,请留下博客,我将确保发布电子版本。
设计文档被链接而不是嵌入到文章文本中,因为如果嵌入,图像宽度将大于 Code Project 的指南,并会引入水平滚动条。
如果您对设计感兴趣,那么它们在这里。
请注意,在开发过程中,设计中的 OnBeforeUpdate
和 OnAfterUpdate
事件变成了 Updating
和 Updated
事件,以符合 Microsoft 推荐的命名约定。
还值得一提的是,当在 bag 中设置属性时,实际发生的是一个 get 操作紧接着一个 set 操作。
考虑以下代码:
/// Set the value of the ZipCode property to 123456
objAddress.Properties["ZipCode"].Value = 123456;
实际发生的是,通过 objAddress.Properties["ZipCode"]
索引的 Property
实例被读取到最终的消费者,然后该 Property
类的 Value
属性被设置。
这只有一行代码,但它调用了 Get 和 Set 序列图操作。
终于开始编码了
本节将逐一介绍代码,解释它在实践中是如何工作的。从快速浏览序列图可以看出,PropertyBag
类所做的工作很少,大部分工作是在 Property
类内部完成的。
PropertyBag 类
PropertyBag
继承自 System.Object
,并包含以下 private
字段。
private System.Collections.Hashtable objPropertyCollection =
new Hashtable();
private System.Object objOwner;
objPropertyCollection
是一个 System.Collections.Hashtable
,其中包含 Property
对象的集合。没有必要构造一个强类型集合,因为属性集合是 private
的,并且从不直接访问。所有访问都通过索引器进行。
Owner
指回包含 PropertyBag
的类(例如示例中的 Address
类)。它在构造函数中传递,并传递给任何实例化的 Property
对象,使它们能够反射父类,以确定某个特定属性(即 Line1
属性)是否是“真实”属性,而不是 PropertyBag
的索引成员。
除了构造函数和一个公开可访问的 Owner
属性外,PropertyBag
中唯一的其他代码是索引器。
/// <summary>
/// Indexer which retrieves a property from the PropertyBag based on
/// the property Name
/// </summary>
public Property this[string Name]
{
get
{
// An instance of the Property that will be returned
Property objProperty;
// If the PropertyBag already contains
// a property whose name matches
// the property required, ...
if (objPropertyCollection.Contains(Name))
{
// ... then return the pre-existing property
objProperty = (Property)objPropertyCollection[Name];
}
else
{
// ... otherwise, create a new Property
// with a matching Name, and
// a null Value, and add it to the PropertyBag
objProperty = new Property(Name, Owner);
objPropertyCollection.Add(Name, objProperty);
}
return objProperty;
}
}
public Property this[string Name]
是 C# 中索引器语法的示例。这将允许类客户端进行类似 objAddress.Properties["ZipCode"]
的调用,预期传入一个字符串,并返回一个 Property
类型的对象。
我一直觉得强制包含 this
关键字令人困惑。毕竟,PropertyBag
类还能索引什么呢,而不是它自身的当前实例?我通过忽略 this
关键字的存在来理解索引器,也许你也会。
索引器内的代码很简单;如果由字符串 Name
索引的 Property
已经存在于 bag 中,则返回它,否则构造一个新的 Property
,传入 Name
和 Owner
,以及一个空的 Value
。
空的 Value
可能看起来不直观,但如果我们记住设计中的脚注,即 set 实际上等于 get 紧接着 set,我们可以看到以下调用。
/// Set the value of the ZipCode property to 123456
objAddress.Properties["ZipCode"].Value = 123456;
其中 "ZipCode"
索引的 Property
不存在,它被创建为一个 null
Value
,通过引用传回客户端,然后它的 Value
属性在两个离散的步骤中被设置,即使它只有一行源代码。
Property 类
PropertyBag
类在其 System.Collections.Hashtable
中保存一个 Property
对象的集合。Property
类完成了所有工作。Property
类是 PropertyBag
类的内部类,但它是 public
的。它必须是 public
的,因为 PropertyBag
索引器返回一个 Property
对象,并且类声明的可见性不能低于返回其类型的某个方法。
我不会浪费时间讨论事件的语法。更多信息可以在 Using Events and Delegates in C# 中找到。
Property
构造函数期望一个 Name
和对 Owner
的引用,并将初始 Value
设置为 null
,如前所述。所有工作都发生在 Value
属性中。
public System.Object Value
{
get
{
// The lock statement makes the
// class thread safe. Multiple threads
// can attempt to get the value of
// the Property at the same time
lock(this)
{
// Use reflection to see if the client
// class has a "real" property
// that is named the same as the
// property that we are attempting to
// retrieve from the PropertyBag
System.Reflection.PropertyInfo p =
Owner.GetType().GetProperty(Name);
if (p != null)
{
// If the client class does have a
// real property thus named, return
// the current value of that "real"
// property
return p.GetValue(Owner, new object[]{});
}
else
{
// If the client class does not
// have a real property thus named,
// return this Property objects
// Value attribute
return this.objValue;
}
}
}
...
}
lock
语句及其在 set
中的匹配语句确保在一个线程获取值时,没有其他线程可以设置它,反之亦然(作为副作用,它阻止了多个线程同时获取 Value
)。
get
使用 System.Reflection.PropertyInfo
类,利用 Owner
对象来确定所有者是否有一个名为(例如 Line1
)的“真实”属性。如果有,则使用 PropertyInfo.GetValue
读取“真实”属性的值,并返回给客户端。如果没有,则返回 Property
实例自身的 Value
。
public System.Object Value
{
...
set
{
// The lock statement makes the class
// thread safe. Multiple threads
// can attempt to set the value of
// the Property at the same time
lock(this)
{
// Reflection is used to see if the client class
// has a "real" property
// that is named the same as the
// property that we are attempting to
// set in the PropertyBag
System.Reflection.PropertyInfo objPropertyInfo =
Owner.GetType().GetProperty(Name);
// Placeholder for the old value
System.Object objOldValue = null;
// If objPropertyInfo is not null, ...
if (objPropertyInfo != null)
{
// ... then the client class has
// a real property thus named,
// save the current value of that
// real property into objOldValue
objOldValue =
objPropertyInfo.GetValue(Owner, new object[]{});
}
else
{
// ... otherwise the client class does
// not have a real property thus
// named, save the current value
// of this Property objects Value attribute
objOldValue = this.objValue;
}
// Create a sub-class of EventArgs to
// hold the event arguments
WebSoft.UpdatingEventArgs objUpdatingEventArgs =
new UpdatingEventArgs(Name, objOldValue, value);
// Execute a synchronous call to each subscriber
OnUpdating(objUpdatingEventArgs);
// If one or more subscribers set the Cancel property
// to true, this means that
// the update is cancelled in an Updating event
// (maybe validation has
// failed), so the the function returns immediately
if (objUpdatingEventArgs.Cancel)
{
return;
}
// If the client class has a "real" property
// matching this Property Name, ...
if (objPropertyInfo != null)
{
// ... then set that "real" property to
// the new value
objPropertyInfo.SetValue(Owner, value, new object[]{});
}
else
{
// ... otherwise, set the Value attribute
// of the current property object
this.objValue = value;
}
// ... Execute a synchronous call to each subscriber
OnUpdated(new UpdatedEventArgs(Name, objOldValue, value));
}
}
}
set
的工作方式类似,但有一些额外的复杂性,以便实现事件。
同样,使用 lock
使类能够安全地进行多线程访问。System.Reflection.PropertyInfo
在 get
中被用来读取“真实”值,或者相应地读取当前的 Property.Value
。这是必需的,以防有 Updating
或 Updated
事件的订阅者,两者都需要旧的(即更新前)值。
实例化一个 WebSoft.UpdatingEventArgs
对象,并将其传递给 protected
方法 OnUpdating
。如果一个或多个订阅者将 WebSoft.UpdatingEventArgs
对象的 Cancel
属性设置为 true
,则通过从属性 set
立即 return
来取消更新。
接下来,再次使用 System.Reflection.PropertyInfo
来调用“真实”属性的 SetValue
,如果存在,则设置当前 Property
实例的 Value
。
最后,调用 OnUpdated
来通知 Updated
事件的所有订阅者。
其余代码实现了 protected
方法来引发事件,简单的属性 getter,以及一个用于 UpdatedEventArgs
的类,以及一个用于 UpdatingEventArgs
的类,后者继承自 UpdatedEventArgs
,通过包含一个 Cancel
属性来扩展它,使 Updating
事件的订阅者能够在不喜欢新值时拒绝该提议。
最后,声明了 UpdatingEventHandler
和 UpdatedEventHandler
的委托。
结论
PropertyBag
类是任何可能需要扩展其标准属性范围的类聚合的一个有用工具。设计演示了使用 UML 进行敏捷设计的方法,并提供了 UML 类图和 UML 序列图的良好示例。研究类的实现提供了索引器、内部类、反射、事件和线程安全的具体示例。