探索动态程序集
Written by Allen Lee
我相信,当你看到标题中“动态程序集”(Dynamic Assembly)这个字眼时,就冒出了很多想法和问题,然而,在我们深入这个概念之前,先来看看我遇到了什么问题。
1. 发生了什么事?
A:我们的客户要处理一组 Shape 对象。
B:怎么处理?
A:计算其中每个对象的面积。 这点需求实在太简单了,不是吗?你只需要建立一个如下图所示的继承体系就可以做到!
Shape 是一个抽象类,它包含一个名为 CalculateArea 的抽象方法,顾名思义,这个方法就是用来计算面积的。此外,上图还展示了一个名为 Triangle 的派生类,它包含了两个三角形的属性:Base(底)和 Altitude(高),当然,它重写了 Shape 的 CalculateArea 方法。依样画葫,我们就可以得到 Circle、Square 了。
现在假设有一个 GetShapeObjects 方法:
IEnumerable<Shape> GetShapeObjects()
{
//
}
那么只需一个 foreach 就可以处理这组 Shape 对象了:
foreach (Shape shape in GetShapeObjects())
{
Console.WriteLine(shape.CalculateArea());
}
A:你的方案很好,事实上,如果我们的客户也是程序员的话,那么你的方案简直可以算得上无懈可击了。不幸的是,我们的客户对程序开发一窍不通,所以你不能指望他们能像你那样在 Visual Studio 里创建 Shape 的派生类,他们希望有一个程序通过一些他们能看懂的提示帮助他们创建这些派生类。
B:……
现在,你应该搞清楚发生了什么事。简而言之,就是帮助那些不懂程序开发的客户在程序运行期间创建出符合他们要求的派生类。
2. 程序员,你如何创建派生类?(C# 版)
在进入正题之前,请允许我再一次离题。回顾一下我们是如何用 C# 创建 Figure #1 中的 Triangle 类:
首先,我们声明一个 Triangle 类,并让它继承 Shape 抽象类。
public class Triangle : Shape
{
}
接着,我们为它添加类型为 double 的 Base 属性。
private double m_Base;
public double Base
{
get { return m_Base; }
set { m_Base = value; }
}
然后,我们为它添加类型为 double 的 Altitude 属性。
private double m_Altitude;
public double Altitude
{
get { return m_Altitude; }
set { m_Altitude = value; }
}
最后,我们重写 CalculateArea 方法。
public override double CalculateArea()
{
return Base * Altitude / 2;
}
人们总是倾向于忽略那些司空见惯的事物,因此,为人们所适应的问题通常不被看作问题。我们对上述的派生类创建过程毫无疑问是非常熟悉的,然而,我们的客户却对此一无所知,因此,显式回顾这个过程将有助于我们思考如何实现类型工厂。
3. 如果我们提供类型工厂……
B:我们可以提供类型工厂。
A:什么?类型工厂?
B:是的,我们可以模仿程序员创建派生类的过程实现一个类型工厂。
A:有意思,能详细一点吗?
B:当然可以。假设 ShapeTypeFactory 就是我们将要提供给客户的类型工厂,那么程序员创建 Triangle 派生类的过程将可以表达成以下代码:
ShapeTypeFactory fac = new ShapeTypeFactory();
fac.CreateShapeType("Triangle");
fac.AddShapeProperty("Base");
fac.AddShapeProperty("Altitude");
fac.ImplementCalculateAreaMethod("Base * Altitude / 2");
fac.SaveShapeType();
A:这个主意听起来不错,你赶快去试一下。
3.1 创建类型
说罢了,动态程序集就是在程序运行时通过发射(Emit)IL 代码生成的程序集,而一旦生成,它就和我们平时看到的程序集没什么两样了。由于这里要生成的是程序集,所以这个过程将会和上面用 C# 创建派生类的过程稍稍不同,其中的区别在于这里我们需要显式定义程序集(以及模块)并将之保存,而上面则通过编译器来完成。
在继续之前,我有必要对生成的程序集做一些约定,拿 Code #07 作为例子,它生成的程序集将会:
- 放在 Shapes 目录中;
- 它的名字为 DynamicShape.Shapes.Triangle.dll;
- 其中 Triangle 类位于 DynamicShape.Shapes 命名空间中。
由于类型的全限定名字将被多次用到,于是我把作为 CreateShapeType 的参数传进来的类型名字作为 ShapeTypeFactory 的成员变量储存起来,并提供一些辅助属性:
class ShapeTypeFactory
{
private string m_ShapeName;
public string ShapeName
{
get { return m_ShapeName; }
}
public string FullQualifiedShapeName
{
get { return String.Format("DynamicShape.Shapes.{0}", m_ShapeName); }
}
public string FileName
{
get { return String.Format("DynamicShape.Shapes.{0}.dll", m_ShapeName); }
}
}
在 CreateShapeType 方法里,我们首先把参数储存在成员变量中:
m_ShapeName = shapeName;
接着,我们创建一个 AssemblyName 的对象实例来存放基本的程序集信息:
AssemblyName assemblyName = new AssemblyName();
assemblyName.Name = FullQualifiedShapeName;
assemblyName.Version = new Version("1.0.0.0");
然后,我们通过 AppDomain.DefineDynamicAssembly 方法创建 AssemblyBuilder 对象实例,由于这个对象实例在整个动态程序集创建过程结束时还需要用到,于是我们把它储存在成员变量中:
m_AssemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(
assemblyName,
AssemblyBuilderAccess.Save,
"Shapes"
);
由于我们希望把生成的动态程序集存放在 Shapes 目录中,于是我们需要以参数的形式给 DefineDynamicAssembly 方法指出,并且也只有此时才能指定动态程序集的生成目录。
再来,就是通过 AssemblyBuilder.DefineDynamicModule 方法创建模块,此时我们需要指定模块的名字以及文件名:
ModuleBuilder moduleBuilder = m_AssemblyBuilder.DefineDynamicModule(
FullQualifiedShapeName,
FileName
);
考验脑力区:
单模块程序集和多模块程序集有什么不同?如何创建多模块程序集?
最后,轮到类型的创建了,通过 ModuleBuilder.DefineType 可以创建 TypeBuilder 对象实例,由于这个对象在后面添加属性以及重写方法时需要用到,于是我把它储存在成员变量中:
m_TypeBuilder = moduleBuilder.DefineType(
FullQualifiedShapeName,
TypeAttributes.Public,
typeof(Shape)
);
在整个类型创建过程结束之时,我们需要调用 TypeBuilder.CreateType 方法以便生成类型,并且调用 AssemblyBuilder.Save 方法保存动态程序集,这也是 SaveShapeType 方法的职责:
public void SaveShapeType()
{
m_TypeBuilder.CreateType();
m_AssemblyBuilder.Save(FileName);
}
3.2 添加属性
在 C# 中为一个类添加一个可读写的属性是一件非常容易的事情(C# 3.0 的自动属性使得这个过程变得更加简洁)。如果你使用 Visual Studio 2005 的话,只需要输入 prop,接着敲击 Tab 键,然后对属性模板稍作修改就可以了!
然而,这个过程在这里将会异常繁杂,这可以归功于属性的本质。在继续之前,我认为有必要先回顾一下属性这个东西。拿 Code #05 作为例子,与之对应的 IL 代码是:
.field private float64 m_Altitude
.method public hidebysig specialname instance float64
get_Altitude() cil managed
{
// Code size 12 (0xc)
.maxstack 1
.locals init ([0] float64 CS$1$0000)
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldfld float64 DynamicShape.Shapes.Triangle::m_Altitude
IL_0007: stloc.0
IL_0008: br.s IL_000a
IL_000a: ldloc.0
IL_000b: ret
}
.method public hidebysig specialname instance void
set_Altitude(float64 'value') cil managed
{
// Code size 9 (0x9)
.maxstack 8
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldarg.1
IL_0003: stfld float64 DynamicShape.Shapes.Triangle::m_Altitude
IL_0008: ret
}
.property instance float64 Altitude()
{
.get instance float64 DynamicShape.Shapes.Triangle::get_Altitude()
.set instance void DynamicShape.Shapes.Triangle::set_Altitude(float64)
}
考验脑力区:
hidebysig 的作用是什么?specialname 又有什么用?
根据上面的代码,我们可以把定义一个属性的步骤归纳为:
- 定义一个私有字段 m_Altitude;
- 定义一个名为 get_Altitude 的方法,该方法返回 m_Altitude 的值;
- 定义一个名为 set_Altitude 的方法,该方法对 m_Altitude 进行设值;
- 定义一个名为 Altitude 的属性,并以 get_Altitude 方法作为读访问器,set_Altitude 方法作为写访问器。
考验脑力区:
属性的读写访问器可以分别具有不同的访问级别吗?
搞清楚属性的本质后,我们就可以动手实现 AddShapeProperty 方法了:
public void AddShapeProperty(string propertyName)
{
FieldBuilder fieldBuilder = m_TypeBuilder.DefineField(
String.Format("m_{0}", propertyName),
typeof(double),
FieldAttributes.Private
);
PropertyBuilder propertyBuilder = m_TypeBuilder.DefineProperty(
propertyName,
PropertyAttributes.None,
typeof(double),
null
);
MethodBuilder getterBuilder = m_TypeBuilder.DefineMethod(
String.Format("get_{0}", propertyName),
MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig,
typeof(double),
Type.EmptyTypes
);
ILGenerator getterILGenerator = getterBuilder.GetILGenerator();
getterILGenerator.Emit(OpCodes.Ldarg_0);
getterILGenerator.Emit(OpCodes.Ldfld, fieldBuilder);
getterILGenerator.Emit(OpCodes.Ret);
propertyBuilder.SetGetMethod(getterBuilder);
MethodBuilder setterBuilder = m_TypeBuilder.DefineMethod(
String.Format("set_{0}", propertyName),
MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig,
null,
new Type[] { typeof(double) }
);
ILGenerator setterILGenerator = setterBuilder.GetILGenerator();
setterILGenerator.Emit(OpCodes.Ldarg_0);
setterILGenerator.Emit(OpCodes.Ldarg_1);
setterILGenerator.Emit(OpCodes.Stfld, fieldBuilder);
setterILGenerator.Emit(OpCodes.Ret);
propertyBuilder.SetSetMethod(setterBuilder);
}
考验脑力区:
无论是读取还是写入字段的值,我们都要先载入第一个参数(OpCodes.Ldarg_0),为什么要这样?这个参数是什么?
此时此刻,不知道你有否这样一番感概:原来编译器在后面默默地为我做了这么多事情!
3.3 重写 CalculateArea 方法
终于到了重写 CalculateArea 方法了,然而,在这个看似简单的环节里却隐藏着巨大的困难。细心观察 Code #07,不难发现我们需要把“Base * Altitude / 2”这样的表达式解析成 IL 代码!我想放弃了,但又不甘心,只好硬着头皮上网找找看……
增值服务区:
《利用堆栈解析算术表达式一:基本过程》
我们传递给 ImplementCalculateAreaMethod 方法的是“Base * Altitude / 2”,而最终解释后符合 IL 堆栈语义的是“Base Altitude * 2 /”,前者叫做“中缀表达式”,而后者则为“后缀表达式”。我们希望解析器除了支持运算符和常量运算数外,还要支持以单词为单位的变量,因为这些变量最终会被重定向到类型所包含的属性。找了很久都没有发现满足要求的解析器,无奈只好自己硬着头皮写一个。
由于我真的很懒,并且本文的主题是动态程序集,于是我只在这里实现一个功能异常有限(甚至不能称之为解析器)的解析器。为此,我制定了如下约束:
- 表达式可包含的符号为常量运算数、算术运算符以及以单词为单位的变量符号;
- 变量符号必须与对应的类型所包含的属性一致;
- 表达式中每个符号之间以空格隔开;
- 表达式(目前)只支持乘法(×)和除法(÷)运算。
下面我利用堆栈把被我高度约束中缀表达式解析成 IL 符号序列:
public class FormulaExpression
{
public static IList Parse(string formulaExpression)
{
ArrayList reversePolishNotation = new ArrayList();
Stack<OpCode> operatorStack = new Stack<OpCode>();
foreach (string token in formulaExpression.Split(' '))
{
OpCode operatorToken;
if (TryParse(token, out operatorToken))
{
if (operatorStack.Count > 0)
{
reversePolishNotation.Add(operatorStack.Pop());
}
operatorStack.Push(operatorToken);
}
else
{
double constant;
if (Double.TryParse(token, out constant))
{
reversePolishNotation.Add(constant);
}
else
{
reversePolishNotation.Add(token);
}
}
}
reversePolishNotation.Add(operatorStack.Pop());
return reversePolishNotation;
}
private static bool TryParse(string operatorToken, out OpCode result)
{
switch (operatorToken)
{
case "*":
result = OpCodes.Mul;
return true;
case "/":
result = OpCodes.Div;
return true;
default:
result = new OpCode();
return false;
}
}
}
在输出的 IL 符号序列里只存在三种类型的符号:类型为 double 的常量运算数、类型为 string 的变量属性名和类型为 OpCode 的 IL 操作码。
至此,我们可以开始重写 CalculateAreaMethod 了:
public void ImplementCalculateAreaMethod(string formulaExpression)
{
MethodBuilder calculateAreaMethodBuilder = m_TypeBuilder.DefineMethod(
"CalculateArea",
MethodAttributes.Public | MethodAttributes.ReuseSlot | MethodAttributes.Virtual | MethodAttributes.HideBySig,
typeof(double),
Type.EmptyTypes
);
ILGenerator calculateAreaMethodILGenerator = calculateAreaMethodBuilder.GetILGenerator();
foreach (object token in FormulaExpression.Parse(formulaExpression))
{
if (token.GetType() == typeof(double))
{
calculateAreaMethodILGenerator.Emit(OpCodes.Ldc_R8, (double)token);
}
else if (token.GetType() == typeof(string))
{
calculateAreaMethodILGenerator.Emit(OpCodes.Ldarg_0);
calculateAreaMethodILGenerator.Emit(OpCodes.Call, m_Properties[(string)token].GetGetMethod());
}
else
{
calculateAreaMethodILGenerator.Emit((OpCode)token);
}
}
calculateAreaMethodILGenerator.Emit(OpCodes.Ret);
}
值得提醒的是,当你重写一个方法时,你只需为其贴上 MethodAttributes.ReuseSlot 和 MethodAttributes.Virtual,剩下的事情 TypeBuilder 会帮你处理的。再者,在获取属性值的时候,我们其实是调用它的读访问器。由于前面通过 AddShapeProperty 方法添加的属性在这里会用到,所以我用了一个 Dictionary 来储存这些属性。这个 Dictionary 在 ShapeTypeFactory 的构造函数里初始化,并在 AddShapeProperty 方法末尾添加条目。
A:你的类型工厂到底是怎么搞的?
B:出了什么事?
A:客户说他们想创建梯形,生成的东西乱七八糟!现在客户很生气,后果很严重!
B:……
4. 再谈重写 CalculateArea 方法
或许客户对梯形情有独钟,无论如何,先来看看梯形的面积公式:(TopBase + BottomBase) * Altitude / 2。这下好了,不但有加号,还有括号,难怪客户说生成的东西乱七八糟。
B:或许,我们可以提供预定义的公式方法,以缓解表达式解析器的不足。
A:真的行吗?
B:应该没问题。
A:请不要再出问题了,你没有收到消息吗,最近高层打算裁员!
B:……
4.1 定义公式
面积公式的本质就是函数,中学课本喜欢把这些公式表达成“y = f(x[, ...])”。这样,梯形公式就可以表达成这样:
S = f(a, b, h) = (a + b) * h / 2
实质上,a、b 和 h 相当于方法的输入参数,S 则相当于方法的返回值,于是,我们可以定义这样一个方法来表达梯形的面积公式:
public static double CalculateTrapezoidArea(double topBase, double bottomBase, double altitude)
{
return (topBase + bottomBase) * altitude / 2;
}
显然,上面这个方法和我们平时看到的方法没什么两样,为了使之与其它方法区分开来,我们有必要为它打上一个标记——FormulaMethodAttribute。现在,让我们考虑一下 FormulaMethodAttribute 里面应该包含一些什么信息。既然我们的客户不懂程序开发,给他们显示 CalculateTrapezordArea 这个字眼很明显是不友好的,再者,客户很有可能希望查看这个方法所使用的面积公式,所以,FormulaMethodAttribute 里面应该包含 FriendlyName 和 FormulaExpression 两个属性:
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public class FormulaMethodAttribute : Attribute
{
private string m_FriendlyName;
public string FriendlyName
{
get { return m_FriendlyName; }
set { m_FriendlyName = value; }
}
private string m_FormulaExpression;
public string FormulaExpression
{
get { return m_FormulaExpression; }
set { m_FormulaExpression = value; }
}
}
现在,我们把 FormulaMethodAttribute 应用到 CalculateTrapezoidArea 上:
[FormulaMethod(FriendlyName = "Trapezoid Area Formula", FormulaExpression = "(TopBase + BottomBase) * Altitude / 2")]
public static double CalculateTrapezoidArea(double topBase, double bottomBase, double altitude)
{
//
}
考验脑力区:
特性(attribute)是在什么时候被实例化的?
4.2 供应公式
我们可以预先定义好很多公式,但如果这些公式仅仅存放在某个角落,就没有达到我们原本的目的了,所以我们需要一个公式士多,以便在需要时给我们供应公式。
由于公式是以方法级别的粒度存在的,所以我们要对指定目录中每个文件中的每个类型中的每个方法进行判定,并把满足要求的方法加载进来。假设所有公式都统一存放在 Formulas 目录里,并且加载时会存放在类型为 IList<T> 的集合 m_FormulaMethods 中。我们的搜寻行动分为三步:
首先,搜寻 Formulas 目录里所有 dll 文件的文件名(不带扩展名);
var files = from file in Directory.GetFiles("Formulas")
where String.Equals(Path.GetExtension(file), ".dll", StringComparison.InvariantCultureIgnoreCase)
select Path.GetFileNameWithoutExtension(file);
foreach (var file in files)
{
//
}
考验脑力区:
什么时候我们不能使用 var 声明变量?
接着,搜寻给定程序集文件里所有满足要求方法:
var methods = from type in Assembly.Load(assemblyName).GetTypes()
from method in type.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Static)
where IsFormulaMethod(method)
select method;
foreach (var method in methods)
{
//
}
考验脑力区:
IEnumerable<T>.Select 方法和 IEnumerable<T>.SelectMany 方法有什么不同?
在这次搜寻里,我们需要判断找到的方法是否满足要求,这是通过 IsFormulaMethod 方法做到:
private bool IsFormulaMethod(MethodInfo method)
{
object[] customAttributes = method.GetCustomAttributes(typeof(FormulaMethodAttribute), false);
return customAttributes.Length == 1;
}
我们的判断标准很简单,只要找到的方法打上了 FormulaMethodAttribute 标记就算满足要求,当然,这个标记只允许打一次,这是在 FormulaMethodAttribute 定义时规定的(AllowMultiple = false)。
最后,就是把找到的满足要求的方法添加到 m_FormulaMethods 集合里。由于从方法提取元数据的操作比较繁杂,于是我们在这里把数据提取出来并储存在一个名为 FormulaMethod 的类中:
FormulaMethodAttribute formulaMethodAttribute = ExtractFormulaMethodAttribute(method);
m_FormulaMethods.Add(
new FormulaMethod
{
FriendlyName = formulaMethodAttribute.FriendlyName,
FormulaExpression = formulaMethodAttribute.FormulaExpression,
MethodInfo = method
}
);
考验脑力区:
对象初始化器对属性有什么要求?
元数据的提取是通过 ExtractFormulaMethodAttribute 方法做到的:
private FormulaMethodAttribute ExtractFormulaMethodAttribute(MethodInfo method)
{
return method.GetCustomAttributes(typeof(FormulaMethodAttribute), false).Cast<FormulaMethodAttribute>().Single<FormulaMethodAttribute>();
}
另外,每当需要时实例化一个公式士多对象,并在指定的目录搜寻和加载所有公式并不是一个好主意,于是,我使用 Singleton 模式来实现公式士多:
private static FormulaMethodStore m_Instance = new FormulaMethodStore();
public static FormulaMethodStore Instance
{
get { return m_Instance; }
}
考验脑力区:
FormulaMethodStore 的对象实例确切是在什么时候被创建的呢?
而 FormulaMethod 类的实现如下:
public class FormulaMethod
{
private string m_FriendlyName;
public string FriendlyName
{
get { return m_FriendlyName; }
set { m_FriendlyName = value; }
}
private string m_FormulaExpression;
public string FormulaExpression
{
get { return m_FormulaExpression; }
set { m_FormulaExpression = value; }
}
private MethodInfo m_MethodInfo;
public MethodInfo MethodInfo
{
get { return m_MethodInfo; }
set { m_MethodInfo = value; }
}
public override string ToString()
{
return m_FriendlyName;
}
}
4.3 重写 CalculateArea 方法
正所谓“养兵千日,用在一时”,现在是时候了,让我们用上刚才所准备的一切吧!慢着,好像还差点什么?!对了,还缺类型属性和方法参数之间的映射。现在,我们需要一个数组,它储存了类型的属性名字,这些名字是按照公式的参数顺序来排列的。这样,我们就可以依次载入属性的值,然后调用公式进行计算:
public void ImplementCalculateAreaMethod(MethodInfo method, string[] propertyNames)
{
MethodBuilder calculateAreaMethodBuilder = m_TypeBuilder.DefineMethod(
"CalculateArea",
MethodAttributes.Public | MethodAttributes.ReuseSlot | MethodAttributes.Virtual | MethodAttributes.HideBySig,
typeof(double),
Type.EmptyTypes
);
ILGenerator calculateAreaMethodILGenerator = calculateAreaMethodBuilder.GetILGenerator();
foreach (string propertyName in propertyNames)
{
calculateAreaMethodILGenerator.Emit(OpCodes.Ldarg_0);
calculateAreaMethodILGenerator.Emit(OpCodes.Call, m_Properties[propertyName].GetGetMethod());
}
calculateAreaMethodILGenerator.Emit(OpCodes.Call, method);
calculateAreaMethodILGenerator.Emit(OpCodes.Ret);
}
B:客户那边有消息了吗?
A:嗯,他们很满意,似乎已经把那个糟糕的表达式解析器忘记了。现在他们有一个小小的要求,就是希望拥有一个类型士多。
B:没问题,只要他们愿意加钱。
A:……
5. 类型士多
类型士多的实现与公式士多的相似,就是对指定目录中每个文件中的每个类型进行判定,并把满足要求的类型加载进来。假设所有类型都统一存放在 Shapes 目录里,并且加载时会存放在类型为 IList<T> 的集合 m_ShapeTypes 中。我们的搜寻行动分为两步:
首先,搜寻 Shapes 目录里所有 dll 文件的文件名(不带扩展名);
var files = from file in Directory.GetFiles(m_Path)
where String.Equals(Path.GetExtension(file), ".dll", StringComparison.InvariantCultureIgnoreCase)
select Path.GetFileNameWithoutExtension(file);
foreach (var file in files)
{
//
}
然后,搜寻给定程序集文件里所有满足要求类型:
var types = from type in Assembly.Load(assemblyName).GetTypes()
where type.IsSubclassOf(typeof(Shape))
select type;
foreach (var type in types)
{
m_ShapeTypes.Add(type);
}
需要提醒的是,这里我使用 Type.IsSubclassOf 方法来判断找到的类型是否为 Shape 的派生类。
A:客户说类型士多有点问题。
B:什么问题?
A:新创建的类型并没有及时地反映到类型士多中,而且没有任何更新途径,他们希望类型士多能够自动检测新创建的类型并更新自身的数据。
B:我明白了。
由于类型士多和公式士多一样,使用了 Singleton 模式来实现,这样,类型的搜寻和加载仅发生在类型士多的对象实例被创建之时,此后即使有新的类型被创建出来,也不会被类型士多发现。为了解决这个问题,我们可以使用 FileSystemWatcher 监视 Shapes 目录,以便当新的类型创建出来时可以把它加载进来。由于 FileSystemWatcher 对象在类型士多对象实例的整个生命周期都存在,我们可以把这个对象声明为一个成员变量 m_Watcher,接着在构造函数里面初始化它:
m_Watcher = new FileSystemWatcher();
m_Watcher.Path = m_Path;
m_Watcher.NotifyFilter = NotifyFilters.LastWrite;
m_Watcher.Filter = "*.dll";
m_Watcher.Created += new FileSystemEventHandler(OnShapeTypeAdded);
m_Watcher.EnableRaisingEvents = true;
我们把 OnShapeTypeAdded 方法挂接到 FileSystemWatcher.Created 事件上,当客户创建了新的类型,这个方法将调用 Code #30 的代码把这个类型加载进来。当然,我们也可以对公式士多如法炮制,让它也具备这种检测并自动更新的功能。
B:客户那边又来新的需求了。
A:什么需求?
B:他们想要一个向导程序。
A:他们愿意加钱吗?
B:你是不是想钱想到疯了?
A:……
下一集,我将会介绍如何为类型工厂创建一个向导程序。