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

Visual Studio 中的属性依赖项生成

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (12投票s)

2009年1月20日

CPOL

4分钟阅读

viewsIcon

35009

downloadIcon

212

一个 VS 插件,用于分析类中的属性依赖关系。

目录

引言

本文介绍了如何克服属性更改通知的一个限制——即它们无法识别属性依赖关系并在多个属性相互依赖时进行传播。最终结果是一个 Visual Studio 插件,它使用静态分析(通过 Cecil 库)来识别属性依赖关系并生成合适的代码隐藏。

问题

考虑一个描述可以投票的人的类。投票者有两个属性——AgeCanVote,假设你需要年满 16 岁才能投票。代码如下所示:

class Voter
{
  private int age;

  public int Age
  {
    get { return age; }
    set { age = value; }
  }

  public bool CanVote
  {
    get
    {
      return Age >= 16;
    }
  }
}

现在想象一下,你希望将这两个属性绑定到某种 UI。你修改类以实现 INotifyPropertyChanged 接口,最终得到以下代码:

class Voter : INotifyPropertyChanged
{
  private int age;

  public int Age
  {
    get { return age; }
    set
    {
      age = value;
      NotifyPropertyChanged("Age");
    }
  }

  public bool CanVote
  {
    get
    {
      return Age >= 16;
    }
  }

  // event and firing code omitted
}

现在,你可以将 Age 属性绑定到 UI,或者监听它的变化。但是,CanVote 呢?毕竟,当 Age 改变时,CanVote 也应该改变。但是,我们无法在任何地方放置 NotifyPropertyChanged() 调用,因为 CanVote 没有 setter。我们该怎么办?

在一个简单的场景中,我们会简单地修改 Age 属性以同时进行通知,如下所示:

public int Age
{
  get { return age; }
  set
  {
    age = value;
    NotifyPropertyChanged("Age");
    NotifyPropertyChanged("CanVote");
  }
}

如你所见,这种解决方案容易出错且难以扩展。如果没有某种自动化机制(例如代码生成),将难以维护。此外,这也很糟糕:你最终会得到关注其他属性通知的属性。这不对!但是,我们该如何解决呢?

解决方案

类变更

让我们暂时假设我们已经拥有了一个属性依赖关系图。在 C# 中,我们如何根据我们的 Voter 类来实现它?首先要做的就是将类声明为 partial,并移除完整的 NotifyPropertyChanged() 实现,只留下一个部分定义。(我们还移除了不必要的通知。)

partial class Voter
{
  private int age;

  public int Age
  {
    get { return age; }
    set
    {
      age = value;
      NotifyPropertyChanged("Age");
    }
  }

  public bool CanVote
  {
    get
    {
      return Age >= 16;
    }
  }

  partial void NotifyPropertyChanged(string propertyName);
}

因此,我们现在有一个更精简的 Voter 类,从中移除了 NotifyPropertyChanged() 实现,因为它不足以处理依赖关系。另外,我还移除了 PropertyChanged 事件——稍后你就会看到它。现在,既然我们已经将类声明为 partial,让我们考虑一下我们的代码隐藏类会包含什么?

代码隐藏类

第一个显而易见的事情是某种依赖列表,指定哪个属性依赖于哪个属性。我的方法是简单地将其定义为一个 Dictionary

// A dictionary of dependencies such that key is the
// independent variable and value is a string[] of dependent variables.
protected Dictionary<string, string[]> dependencies;

现在我们有了 Dictionary,我们还定义了一个函数来用我们的数据初始化它:

// Since a Dictionary cannot be initialized inline, it has to be
// initialized conditionally.
private void InitializeDependencies()
{
  dependencies = new Dictionary<string, string[]>();
  dependencies.Add("Age", new [] { "CanVote" });
}

最后,我们实现了 partial NotifyPropertyChanged() 函数,以利用我们的 Dictionary。在此实现中,我们只是惰性初始化 Dictionary,然后在使用它时使用它。请注意,没有循环依赖检查。

// This notification has to be invoked by any property that
// needs to notify users of its changes.
partial void NotifyPropertyChanged(string propertyName)
{
  if (dependencies == null) InitializeDependencies();

  if (PropertyChanged != null)
  {
    PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    if (dependencies.ContainsKey(propertyName))
      foreach (string s in dependencies[propertyName])
        NotifyPropertyChanged(s);
  }
}

最后,我们还将事件添加到此类。毕竟,我们原始Voter 类不再实现 INotifyPropertyChanged——而是部分类实现的!

分析器

我刚才描述的可能看起来像魔法。除了开发人员之外,谁会知道 CanVote 属性可以依赖于 Age 属性?实际上,此信息已编译到 IL 中,我们可以使用像 Cecil1 这样的库提取它。分析器的源代码已附带,但这里是对分析器如何查找信息的简要描述:

  1. 首先,我们定位已编译的程序集,并确定所选文件的类和命名空间。
  2. 然后,我们通过查看 IL 并识别哪个属性使用了哪个其他属性来获取依赖关系列表。属性 get 很容易找到——我们只需要查找以 get_ 开头的方法。
  3. 当我们获得依赖关系列表时,我们需要反转它,因为我们需要一个影响列表。我的意思是,最初,我们得到一个列表,其中 A 依赖于 B(A ← B),但我们的字典需要指定 B 影响 A(B → A)的列表。
  4. 结果数据使用模板发出到原始类的代码隐藏中。

结论

本文演示了如何自动生成属性依赖关系图。与我之前的文章2 3 不同,此生成器使用 IL 分析而不是源代码分析。诚然,这种分析器不太适合我在这里进行的此类代码生成,但它们功能强大,并且可用于分析 C# 以外的语言编写的程序集。

参考文献

  1. 一个免费的 IL 解析库;可在 http://www.mono-project.com/Cecil 找到
  2. 一个用于创建 C# 装饰器/代理的工具,https://codeproject.org.cn/KB/codegen/decorators.aspx
  3. 一个用于创建 C# 访问者的工具,https://codeproject.org.cn/KB/codegen/visitortool.aspx
© . All rights reserved.