Visual Studio 中的属性依赖项生成






4.75/5 (12投票s)
一个 VS 插件,用于分析类中的属性依赖关系。
目录
引言
本文介绍了如何克服属性更改通知的一个限制——即它们无法识别属性依赖关系并在多个属性相互依赖时进行传播。最终结果是一个 Visual Studio 插件,它使用静态分析(通过 Cecil 库)来识别属性依赖关系并生成合适的代码隐藏。
问题
考虑一个描述可以投票的人的类。投票者有两个属性——Age
和 CanVote
,假设你需要年满 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 这样的库提取它。分析器的源代码已附带,但这里是对分析器如何查找信息的简要描述:
- 首先,我们定位已编译的程序集,并确定所选文件的类和命名空间。
- 然后,我们通过查看 IL 并识别哪个属性使用了哪个其他属性来获取依赖关系列表。属性
get
很容易找到——我们只需要查找以get_
开头的方法。 - 当我们获得依赖关系列表时,我们需要反转它,因为我们需要一个影响列表。我的意思是,最初,我们得到一个列表,其中 A 依赖于 B(A ← B),但我们的字典需要指定 B 影响 A(B → A)的列表。
- 结果数据使用模板发出到原始类的代码隐藏中。
结论
本文演示了如何自动生成属性依赖关系图。与我之前的文章2 3 不同,此生成器使用 IL 分析而不是源代码分析。诚然,这种分析器不太适合我在这里进行的此类代码生成,但它们功能强大,并且可用于分析 C# 以外的语言编写的程序集。
参考文献
- 一个免费的 IL 解析库;可在 http://www.mono-project.com/Cecil 找到
- 一个用于创建 C# 装饰器/代理的工具,https://codeproject.org.cn/KB/codegen/decorators.aspx
- 一个用于创建 C# 访问者的工具,https://codeproject.org.cn/KB/codegen/visitortool.aspx