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

任何类的无限基线

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (6投票s)

2009年6月11日

CPOL

9分钟阅读

viewsIcon

23940

downloadIcon

148

使用 ComponentModel 命名空间实现无限制基线。

image011.png

引言

非常概括地说,基线是系统的一种保留状态。通过将当前状态与以前持久化的状态之一进行比较来衡量进展。从编程的角度来看,保存基线意味着获取对象当前属性的快照。创建的基线越多,对象每个属性的历史就越长。

一个简单的类

业界对浏览和修改对象属性有大量支持。一个例子是 Visual Studio 中的属性网格。还有其他网格根据属性名称显示对象的属性。在我看来,以属性形式存储基线信息是可行的方法。

让我们从一个非常简单的类开始,并逐步改进它以支持无限基线。

PropertyGrid 基础知识

既然我打算大量处理属性和属性描述符,我最好对首选的调试工具:PropertyGrid 有一个基本的了解。如果 PropertyGrid 听从我的命令,这意味着我正朝着正确的方向前进。否则,我可能只会创建一个包含基线属性作为 DataColumnDataTable。但是,那有什么乐趣呢?

让我们创建一个 Person 类,其属性之一是 Address 类型。

class Address
{
    public string City
    {
        get;
        set;
    }

    public string Street
    {
        get;
        set;
    }
}

class Person
{
    public Guid Guid
    {
        get;
        set;
    }


    public string Name
    {
        get;
        set;
    }


    public int Age
    {
        get;
        set;
    }


    public Address Address
    {
        get;
        set;
    }
}

如果我们创建一个带有 PropertyGrid 的表单,并将一个 Person 实例分配给网格,结果将如下所示:

image001.png

所有属性都处于其默认值。Address 属性显示为灰色,因为它不可编辑。要启用自定义类的属性编辑,这些类必须有一个相关的 TypeConverter 类,该类实现从字符串转换。也就是说,实现 CanConvertFromConvertFrom 字符串。MSDN 建议继承自 ExpandableObjectConverter 以简化此任务。(参见 MSDN 中的“添加可扩展属性支持”章节)。

让我们就这样做。创建一个继承自 ExpandableObjectConverterAddressConverter 类;重写 CanConvertToConvertToCanConvertFromConvertFrom 方法。

CanConvertTo 告知给定的类型转换器是否可以转换为目标类型。AddressConverter 可以转换为 Address 类型,因此当目标类型是 Address 时返回 true。

public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
    if (destinationType.IsAssignableFrom(typeof(Address)))
    {
        return true;
    }

    return base.CanConvertTo(context, destinationType);
}

CanConvertFrom 告知给定的类型转换器是否可以从源类型转换。AddressConverter 可以从字符串类型转换,在这种情况下返回 true

public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
    if (sourceType == typeof(string))
    {
        return true;
    }

    return base.CanConvertFrom(context, sourceType);
}

为了避免为我们的类的字符串表示形式设计格式,特别是避免解析该字符串,我选择让 XmlSerializer 类为我完成所有繁重的工作。

ConvertToAddress 序列化为字符串。

public override object ConvertTo(ITypeDescriptorContext context, 
       System.Globalization.CultureInfo culture, object value, Type destinationType)
{
    if (destinationType == typeof(string) && value is Address)
    {
        return SerializeAddressAsString(value as Address);
    }

    return base.ConvertTo(context, culture, value, destinationType);
}

private string SerializeAddressAsString(Address address)
{
    if (address == null)
    {
        return string.Empty;
    }

    StringBuilder sb = new StringBuilder();
    using (XmlWriter xwriter = XmlWriter.Create(sb))
    {
        XmlSerializer addressSerializer = new XmlSerializer(typeof(Address));
        addressSerializer.Serialize(xwriter, address);
        return sb.ToString();
    }
}

ConvertFrom 从字符串反序列化 Address 实例。

public override object ConvertFrom(ITypeDescriptorContext context, 
       System.Globalization.CultureInfo culture, object value)
{
    if (value is string)
    {
        return DeserializeAddressFromString(value as string);
    }

    return base.ConvertFrom(context, culture, value);
}

private Address DeserializeAddressFromString(string serializedAddress)
{
    if (string.IsNullOrEmpty(serializedAddress))
    {
        return null;
    }

    XmlSerializer addressSerializer = new XmlSerializer(typeof(Address));
    return (Address)addressSerializer.Deserialize(new StringReader(serializedAddress));
}

TypeConverterAttribute 应用于 Address 类型,以告知它使用哪个类型转换器

[TypeConverterAttribute(typeof(AddressConverter))]
public class Address

现在运行程序会显示一个可编辑的 Address 框。我们应该在其中输入一个 XML 字符串;例如,这个:

<address><city>Whitehorse</city><street>1 Alexander St</street></address> 

这是一个有效的 XML,可以反序列化为 Address 对象。应用程序中的 PropertyGrid 现在正确显示 Address 的两个属性。

image003.png

趁此机会,让我们更进一步。虽然 XML 字符串运行良好且实现起来轻而易举,但它需要一些输入。更重要的是,它需要了解类名和类属性才能首先构建 XML。这不是很用户友好,甚至令人恼火。让我们改显示一个带有两个输入字段的小表单。也就是说,制作一个自定义属性编辑器,就像所有自重的组件发布者一样。

地址类型属性的自定义 UI

PropertyGrid 将为具有关联 UITypeEditor 的属性显示自定义用户界面。要创建功能齐全的自定义 UI 类型编辑器,需要几个简单的步骤。

首先,我们需要一个 UI 类来显示 Address 属性。UI 类可以显示为模态表单或下拉列表。我选择将其实现为模态表单。我的 AddressEditorForm 类包含两个文本框和两个公共属性来访问这些文本框的内容。

接下来,我们需要告诉 PropertyGrid 如何使用 AddressEditorForm 来编辑 Address。这是 AddressUITypeEditor 的职责,它继承自 UITypeEditorAddressUITypeEditor 指示 PropertyGrid 如何显示 UI 组件。它充当显示给用户的 UI 和网格中的属性之间的桥梁。

重写 GetEditStyle 方法,以指示 PropertyGrid 如何显示 AddressEditorFormAddressEditorForm 显示为 UITypeEditorEditStyle.Modal

重写 EditValue 方法以显示 AddressEditorForm,并使用它返回的值来设置新的 Address 值或更新现有值。

class AddressUITypeEditor : UITypeEditor
{
    public override UITypeEditorEditStyle 
      GetEditStyle(System.ComponentModel.ITypeDescriptorContext context)
    {
        return UITypeEditorEditStyle.Modal;
    }

    public override object EditValue(System.ComponentModel.ITypeDescriptorContext 
           context, IServiceProvider provider, object value)
    {
        IWindowsFormsEditorService service = 
          (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService));
        Address address = value as Address;

        AddressEditorForm addressEditor = new AddressEditorForm();
        if (address != null)
        {
            addressEditor.Street = address.Street;
            addressEditor.City = address.City;
        }
        
        if (DialogResult.OK == service.ShowDialog(addressEditor))
        {
            return new Address
            {
                City = addressEditor.City,
                Street = addressEditor.Street
            };
        }

        return address;
    }
}

当程序运行时,此代码会产生什么结果?

image005.png

单击 Address 属性内部会显示省略号,这反过来会在模态表单中弹出 AddressEditorFormAddress 仍然序列化为 XML 字符串并从 XML 字符串反序列化,但现在不再需要记住类结构并手动输入。

至此,Person 类已转换为一个可以轻松使用 PropertyGrid 进行编辑的类。我们可以继续讨论本文的主要主题:无限基线。

选择要跟踪的属性

Person 的一个实例 Bob 诞生了。Bob 的哪些属性可能会改变?GuidBob 的整个生命周期中将保持不变。姓名“Bob”很可能也不会改变。我们感兴趣的是跟踪两个属性:Bob 的年龄和地址。这些感兴趣的属性将由 BaselineProperty 属性标记。BaselinePropertyAttribute 只能应用于类的属性。

[AttributeUsage(AttributeTargets.Property)]
class BaselinePropertyAttribute : Attribute
{
}

这些是 Person 类的更改:

[BaselineProperty]
public int Age
{
    get;
    set;
}

[BaselineProperty]
public Address Address
{
    get;
    set;
}

动态添加和删除对象属性

基线将作为动态可绑定对象属性实现。Person 类需要能够随意添加和删除这些属性,这意味着该类需要提供自身的类型信息。也就是说,该类要么实现 ICustomTypeDescriptor 接口,要么继承自 CustomTypeDescriptor 类。

在本示例中,继承自 CustomTypeDescriptor 更简单。我们只对调整描述类属性的功能感兴趣。总的来说,有三个函数需要重写才能恢复我们之前的默认行为。

class Person :  CustomTypeDescriptor
{
...
...
    public override PropertyDescriptorCollection 
                    GetProperties(Attribute[] attributes)
    {
        // TODO: Replicate baseline properties.
        return TypeDescriptor.GetProperties(this, attributes, true);
    }


    public override PropertyDescriptorCollection GetProperties()
    {
        return GetProperties(null);
    }


    public override object GetPropertyOwner(PropertyDescriptor pd)
    {
        return this;
    }
...
...
}

部分地,基线魔术将发生在 Person 类的 GetProperties() 方法中。

我们还没有添加或移除基线的用户界面。这对于一个简单的概念验证来说不是必需的。假设添加了一个基线,比如说基线 1。用 BaselinePropertyAttribute 标记的属性是 AddressAge。如果基线 1 存在,我希望看到对象 Bob 的以下属性:AddressAgeGuidNameAddress1Age1

GetProperties() 方法被修改以创建一个包含复制基线属性的属性描述符集合。要复制基线属性,我们将复制那些被 BaselinePropertyAttribute 标记的属性的属性描述符,然后将基线属性描述符与默认属性描述符组合成一个集合。

public override PropertyDescriptorCollection GetProperties(Attribute[] attributes)
{
    List<PropertyDescriptor> defaultPds = 
        TypeDescriptor.GetProperties(this, attributes, true).Cast<PropertyDescriptor>().ToList();

    IEnumerable<PropertyDescriptor> baselinePds =
        defaultPds.Where(x => x.Attributes.Contains(new BaselinePropertyAttribute()));

    // Array of replicated property descriptors, which describe baseline properties.
    List<PropertyDescriptor> replicatedBaselinePds = new List<PropertyDescriptor>();

    // Assume there is a baseline. Assume it is "baseline 1".
    List<int> baselines = new List<int>(new int[] { 1 });
    foreach (int baseline in baselines)
    {
        foreach (PropertyDescriptor pd in baselinePds)
        {
            replicatedBaselinePds.Add(new BaselinePropertyDescriptor(pd, baseline));
        }
    }
    return new PropertyDescriptorCollection(defaultPds.Union(replicatedBaselinePds).ToArray());
}

由于基线属性的行为与原始属性相同,因此基线属性应具有几乎相同的 PropertyDescriptorPropertyDescriptor 不是可克隆的类。复制它的最佳方法是创建子类并使用其中一个初始化器重载来传递另一个 PropertyDescriptor 实例。不幸的是,这不允许更改 PropertyDescriptorName 属性,这在本文中是绝对必需的。例如,属性 Age 的描述符必须复制并赋予新名称 Age1

没有同时完整复制 PropertyDescriptor 并允许更改 Name 的初始化器。次佳的方法是将 Name 设置为所需值,并尽可能多地使用现有 PropertyDescriptor。原始属性的属性描述符将被子类封装,并用于实现子类的抽象成员。

class BaselinePropertyDescriptor : PropertyDescriptor
{
    PropertyDescriptor _baseDescriptor = null;

    public BaselinePropertyDescriptor(PropertyDescriptor descriptor, int baseline)
        : base(GetPdName(descriptor, baseline), GetPdAttribs(descriptor))
    {
        _baseDescriptor = descriptor;
    }


    private static string GetPdName(PropertyDescriptor descriptor, int baseline)
    {
        return string.Format("{0}{1}", descriptor.Name, baseline);
    }

    
    private static Attribute[] GetPdAttribs(PropertyDescriptor descriptor)
    {
        return descriptor.Attributes.Cast<Attribute>().ToArray();
    }


    public override bool CanResetValue(object component)
    {
        return _baseDescriptor.CanResetValue(component);
    }


    public override Type ComponentType
    {
        get { return _baseDescriptor.ComponentType; }
    }


    public override object GetValue(object component)
    {
        return _baseDescriptor.GetValue(component);
    }


    public override void ResetValue(object component)
    {
        _baseDescriptor.ResetValue(component);
    }


    public override void SetValue(object component, object value)
    {
        _baseDescriptor.SetValue(component, value);
    }
...
}

让我们运行程序,看看按照本示例的方式实现 GetProperties()BaselinePropertyDescriptor 所产生的效果。

image007.png

基线属性 Address1Age1 都在!但是,更改 Age 的值会修改 Age1 的值,反之亦然。同样的问题也发生在 Address 字段上。这是怎么回事?

答案很明显:这是 BaselinePropertyDescriptor 的实现方式。我们已经更改了属性的名称,但我们使用了原始 PropertyDescriptorGetValue()SetValue() 方法。这些方法当然会修改原始的 Bob.Age 属性。这些函数不会查找属性 Bob.Age1,仅仅因为它们是从 PropertyDescriptor 调用,而该 PropertyDescriptorNameAge1GetValueSetValue 方法必须修改以访问 Age1 的值,无论该值可能在哪里。要完成的两个任务是:首先,基线属性的值必须存储在某个地方。其次,GetValue()SetValue()ResetValue()ShouldSerialize() 方法必须知道如何访问内存中的这些属性。

存储基线属性值

用于存储基线属性的数据结构是一个设计问题。我将继续采用我脑海中出现的第一个想法,因为我没有找到更简单、更容易、更清晰的解决方案。也许实际的类会需要一个完全不同的解决方案,但我的 Person 类并没有特别地暗示任何东西。

基线属性的值将存储在 Person 类中的一个私有字典中。键将是属性名称,例如 Age1Address1 等。值将是存储为对象的基线属性值。数据结构将是 Dictionary<string>。其内容将类似于以下内容:

Age1 --> 1
Address1 --> 123 SomeStreet, SomeCity
Age2 --> 2
Address2 --> 123 SomeStreet, SomeCity

从 PropertyDescriptor 访问基线数据

现在基线数据存储在字典中,我们需要一种从 BaselinePropertyDescriptor 访问它的方法。同样,访问此数据的方法将根据具体设计而大相径庭。在 Person 类中创建两个私有方法:GetBaselineProperty()SetBaselineProperty() 来操作基线数据似乎是合理的。这些私有方法将使用反射从 BaselinePropertyDescriptor 调用。

例如,要设置基线属性 Age1,将从 PropertyDescriptor 调用:SetBaselineProperty("Age1", 1)。要获取基线值 Age1,将从 PropertyDescriptor 调用:GetBaselineProperty("Age1")

Person 类进行的更改

private Dictionary<string> _BaselineData = new Dictionary<string>();


private void SetBaselineProperty(string propertyName, object value)
{
    if (!_BaselineData.ContainsKey(propertyName))
    {
        throw new MissingFieldException(this.GetType().Name, propertyName);
    }

    _BaselineData[propertyName] = value;
}


private object GetBaselineProperty(string propertyName)
{
    if (!_BaselineData.ContainsKey(propertyName))
    {
        throw new MissingFieldException(this.GetType().Name, propertyName);
    }

    return _BaselineData[propertyName];
}

BaselinePropertyDescriptor 类进行的更改

public override object GetValue(object component)
{
    Person person = (Person)component;

    // Get a specific baseline value of class Person by calling the
    // GetBaselineProperty method.
    Type t = typeof(Person);
    MethodInfo getBaselineProperty = t.GetMethod("GetBaselineProperty", 
               BindingFlags.NonPublic | BindingFlags.Instance);
    return getBaselineProperty.Invoke(person, new object[] { Name });
}


public override void SetValue(object component, object value)
{
    Person person = (Person)component;

    // Set a specific baseline value of class Person by calling the
    // SetBaselineProperty method.
    Type t = typeof(Person);
    MethodInfo setBaselineProperty = t.GetMethod("SetBaselineProperty", 
                                     BindingFlags.NonPublic | BindingFlags.Instance);
    setBaselineProperty.Invoke(person, new object[] { Name, value });
}


public override void ResetValue(object component)
{
    Person person = (Person)component;
    object value = Activator.CreateInstance(_baseDescriptor.PropertyType);
    SetValue(component, value);
}


public override bool ShouldSerializeValue(object component)
{
    Person person = (Person)component;
    object defaultValue = Activator.CreateInstance(_baseDescriptor.PropertyType);
    object currentValue = GetValue(component);

    return !object.Equals(currentValue, defaultValue);
}

此时,基线数据存储在 Person 类的内存中的适当位置。此数据使用反射从 BaselinePropertyDescriptor 类访问。现在是时候运行程序,看看这些更改会产生什么结果了。

image009.png

完美!概念验证成功。

收尾工作

核心功能已完成。一个概念验证测试用例创建了一个有效的基线。现在,是时候完成其余的基本基线功能了。而且,所谓基本,我指的是添加基线和移除基线。

Person 类获取一个存储现有基线的整数列表。它还获取添加基线、移除基线和验证基线是否存在的功能。

internal bool BaselineExists(int baseline)
{
    return _Baselines.Any(x => x == baseline);
}


internal void AddBaseline(int baseline)
{
    if (BaselineExists(baseline))
    {
        throw new InvalidOperationException(
              string.Format("Baseline {0} exists", baseline));
    }

    _Baselines.Add(baseline);

    IEnumerable<PropertyDescriptor> defaultPds =
        TypeDescriptor.GetProperties(this, true).Cast<PropertyDescriptor>();
    IEnumerable<PropertyDescriptor> baselinePds =
        defaultPds.Where(x => x.Attributes.Contains(new BaselinePropertyAttribute()));

    foreach (PropertyDescriptor pd in baselinePds)
    {
        string strPropertyName = string.Format("{0}{1}", pd.Name, baseline);
        _BaselineData[strPropertyName] = null;
    }
}

用户界面使用这些方法来添加和移除基线。

以下是添加基线 1、5 和 6 的最终结果。

image011.png

一切都正常运行,甚至达到了无聊的程度。

© . All rights reserved.