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

在 C# 中实现 PropertyBag

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (22投票s)

2005年11月25日

14分钟阅读

viewsIcon

213909

downloadIcon

4235

设计和实现 C# 中的 PropertyBag。

引言

本文详细介绍了 C# 中 PropertyBag 类的设计、编码和文档。

在设计类时,会赋予它们设计者设想的客户端所需的属性。例如,一个 Address 类可能需要 PostCodeStreetName 属性,这些属性会被设计到类中。

类经常会在设计者意图范围之外被使用。例如,我们假设的 Address 类开始在美国地区使用,而设计者并未设想有非英国的客户。因此,该类现在需要一个 ZipCode 属性。

一方面,可以扩展 Address 类的代码,重新编译程序集,并重新分发。另一方面,如果 Address 类公开了一个 PropertyBag 对象,那么该类的使用者可以为 PropertyBag 分配临时属性,从而避免了成本高昂的编码、编译和分发。

PropertyBag 类的要求

获取和设置属性

PropertyBag 的主要要求是使客户端类能够添加新属性,为这些属性分配和重新分配值,并读取这些属性的当前值。

易用性

一个次要功能是设计和实现该类,使其接口非常直观且易于使用。为了实现这一点,在访问 Property 类时,PropertyBag 类使用了索引器。

考虑过类型安全的集合,但决定避免过度复杂化代码;类型安全的集合将是后续文章的主题。

使用了内部类来简化 PropertyBag 接口,这将在本文中进一步演示。

直观地处理现有属性

另一个功能是考虑包含 PropertyBag 的类的“真实”属性。

例如,如果父类是 Address 类(它已经描述了一个名为 Line1 的真实属性),并且 Address 类的客户端尝试向 Address 类的 PropertyBag 添加一个 PostCode 属性,那么 PropertyBagPostCode 属性将指向 Address 类中的“真实”属性,而不是 PropertyBag 中具有重复名称的临时属性。

PropertyBag 类中使用反射将能够实现此要求。

在单个属性上实现更新和已更新事件

PropertyBag 类中的属性需要实现事件,以便当 PropertyBag 中的特定属性被更新时,事件订阅者能够收到通知。要实现的事件是 UpdatingUpdatedUpdating 事件将包括一个机制,用于向生成事件的属性信号回该更新将被取消,从而使事件处理程序能够验证拟议的新值,并在验证失败时拒绝新值。

本文假定读者对 C# 中事件和委托的实现有很好的理解。这个主题在文章 Using Events and Delegates in C# 中进行了介绍。

例如,如果 Address 类中的 PropertyBag 可以被客户端用来添加一个新的 ZipCode 属性。添加 ZipCode 属性的类可以订阅 Updating 事件,并拒绝所有非数字的新值。

可序列化性

PropertyBagProperty 对象需要是可序列化的,这样,只要所有者对象(例如本例中的 Address 类)是可序列化的,那么 PropertyBagProperty 对象也将被序列化。

线程安全

最后一个要求是 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 类具有内置的 Line1Line2Line3CityCountyCountryPostCode 属性。它还有一个 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 StreamFormatter 对象可用于序列化和反序列化容器/所有者对象(即 Address 类的实例),包括其聚合的 PropertyBagProperty 对象实例,前提是所有者对象本身是可序列化的。

调用序列化和反序列化的代码如下。

/// 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 的指南,并会引入水平滚动条。

如果您对设计感兴趣,那么它们在这里。

请注意,在开发过程中,设计中的 OnBeforeUpdateOnAfterUpdate 事件变成了 UpdatingUpdated 事件,以符合 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,传入 NameOwner,以及一个空的 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.PropertyInfoget 中被用来读取“真实”值,或者相应地读取当前的 Property.Value。这是必需的,以防有 UpdatingUpdated 事件的订阅者,两者都需要旧的(即更新前)值。

实例化一个 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 事件的订阅者能够在不喜欢新值时拒绝该提议。

最后,声明了 UpdatingEventHandlerUpdatedEventHandler 的委托。

结论

PropertyBag 类是任何可能需要扩展其标准属性范围的类聚合的一个有用工具。设计演示了使用 UML 进行敏捷设计的方法,并提供了 UML 类图和 UML 序列图的良好示例。研究类的实现提供了索引器、内部类、反射、事件和线程安全的具体示例。

© . All rights reserved.