XAML完全不同






4.92/5 (45投票s)
使用标记扩展来构建具有XAML的独立的基于标记的声明式系统。
引言
本文展示了XAML的强大功能以及如何将其用于超越众所周知的W*F实现。
如今,许多人已经熟悉微软的W*F实现,无论是WPF、WCF还是WF,但并非所有人都意识到XAML的强大功能以及如何将其用于完全不同的事物,因此本文展示了一些后台使用的技术以及如何使用它们。即使这个特殊的示例对您来说似乎毫无用处,它仍然可以帮助您设计自己的实现。
使用XAML代替纯XML来描述对象,您将获得一个具有语法高亮、代码补全和智能感知功能的编辑器,以及加载、解析和对象实例化引擎 – 所有这些都无需编写一行代码。
此示例展示了一种基于Markup Extensions的简单声明式脚本语言。它似乎是一个完全多余的YASL(YetAnotherScriptingLanguage),但其轻量级的实现和易用性具有独特的魅力。它可以成为许多需要脚本或类似脚本配置的事物的基础,例如集成测试,您需要语句来描述用户操作和验证语句,而且由于您可以轻松地将命令集限制在完成工作所需的范围内,因此编写脚本的学习曲线比使用拥有数千个与工作无关的函数的更强大的语言要好得多。

由于本文主要侧重于XAML和Markup Extensions的使用,我将只描述这部分;如果您想深入了解更多细节,请查看项目的源代码。
想法
程序就是一个表达式树,所以它是XML的理想选择。一个非常简单的程序如下所示

您可能已经注意到,在Main
节点中声明的命名空间只是我们项目的命名空间 – 这里没有W*F。我们只能通过这种方式创建我们自己程序集中的对象,但没关系,因为目前我们不需要其他任何东西。
假设MarkupScript
命名空间包含类'Main
'、'Echo
'、'If
'等,我们现在可以从C#调用它
Main main = (Main)XamlReader.Load(fileStream);
foreach (var statement in main.Statements)
{
statement.Evaluate();
}
XAML中声明的所有对象都将通过这个简单的调用实例化,我们的表达式树已准备好执行。调用栈只是由对象树中的层级表示,执行只是遍历表达式树。
利用Markup的所有优势
为了利用XAML提供的所有优势,有三个类我们应该特别关注
MakrupExtension
类型转换器
ContentPropertyAttribute
MarkupExtension
对象可以使用简单的类'Main
'、'Echo
'、'If
'等来实现。您将能够用XAML编写上述内容并使用XAML读取器加载它。只缺少一个基本的东西:对象可能需要相互了解。虽然节点很容易了解它们的子节点,但反过来可能需要为每个属性或集合编写额外的代码来用其父对象进行初始化。这时MarkupExtension
类就派上用场了,所以我们树中的每个对象都派生自它
public abstract class Node : MarkupExtension, INode
{
...
#region MarkupExtension overrides
public override object ProvideValue(IServiceProvider serviceProvider)
{
// If the target object is an INode, that's our parent:
if (serviceProvider != null)
{
if (Parent == null)
{
IProvideValueTarget provideValueTarget =
(IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));
Parent = provideValueTarget.TargetObject as INode;
}
}
// Inplace implementation, just return ourselves.
return this;
}
#endregion
}
当XML加载器将MarkupExtension
对象分配给属性时,它会调用MarkupExtension.ProvideValue
,以便扩展能够提供正确的对象。我们不需要提供一个额外的对象,所以我们可以简单地返回扩展对象本身,但我们想要记住分配给我们的目标对象,因为那是我们的父对象。
类型转换器
现在,由于我们想构建一个表达式树,大多数属性都期望一个表达式对象被赋值给它们,所以所有这些属性的类型都是IExpression
。如果我们将其他实现了IExpression
的对象显式分配给属性,那么赋值将是直接的;然而,我们也可能希望通过简单地键入值而不是每次都构造一个常量表达式来赋值常量值。这正是TypeConverters
的用途所在
/// <summary>
/// Simple type converter to allow setting constant expressions
/// by simply typing the value in XAML.
/// </summary>
public class ExpressionTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string);
}
public override object ConvertFrom
(ITypeDescriptorContext context, CultureInfo culture, object value)
{
string stringValue = (string)value;
bool booleanResult;
if (bool.TryParse(stringValue, out booleanResult))
{
return new ConstantExpression(booleanResult);
}
double doubleResult;
if (double.TryParse(stringValue, NumberStyles.Any,
CultureInfo.InvariantCulture, out doubleResult))
{
return new ConstantExpression(doubleResult);
}
return new ConstantExpression(value);
}
}
此类型转换器将接受任何string
值并用ConstExpression
将其包装起来,因此它可以被分配给任何期望IExpression
的地方。现在您可能会问XAML读取器如何知道此转换器的存在以及在哪里应用它。答案是TypeConverterAttribute
,我们只需将其分配给IExpression
接口
[TypeConverter(typeof(ExpressionTypeConverter))]
public interface IExpression : INode
{
...
这样,当目标属性的类型为IExpression
时,任何不是派生自IExpression
的对象都将通过ExpressionTypeConverter
。
ContentProperty
这里使用的另一个特定于Markup的属性是ContentPropertyAttribute
。正如您可能已经注意到的,例如,Main
对象直接包含多个语句,这些语句实际上是Markup Extensions,而Markup Extensions应该被分配给属性。如果我们查看Main
类的代码,我们将看到它是如何完成的
[ContentProperty("Statements")]
public class Main : Library
{
...
public NodeCollection Statements { get; private set; }
ContentPropertyAttribute
告诉XAML读取器将Main
节点的所有内容添加到其Statements
属性。由于可以有多个语句,此属性需要是对象的集合。由于我们在这里使用类型化集合,XAML编辑器的智能感知将只显示有效的语句

如何使用它
创建一个新的脚本
- 创建一个扩展名为“.xaml”的新文本文件。您可以使用任何扩展名,但如果您想利用VisualStudio对XAML文件的智能感知,您应该坚持使用默认扩展名,以免混淆VS。
- 将此XAML文件添加到VS项目中。这是必需的,因为VS编辑器需要项目引用来解析命名空间 - 所以请确保您的项目引用了
MarkupScript
项目或可执行文件。如果您只是在玩,您可以选择将新的XAML文件直接添加到MarkupScript
项目中,就像我在测试文件中所做的那样。 - 在文件的属性窗口中,将“Build Action”设置为“None” – 否则您将收到生成错误。可选地将“Copy to Output Directory”设置为“Copy always”,以便您的脚本靠近可执行文件。
- 用以下内容初始化新文件
现在您已准备好开始编写脚本。请参阅提供的示例作为参考,其中的注释应该能很好地概述您可以做什么以及如何使用它。
运行一个脚本
编译完MarkupScript.exe后,只需将其与脚本名称作为第一个参数调用即可。可以为脚本提供额外的参数。如果脚本定义了某些参数,您可以通过调用“MarkupScript <ScriptFileName> /?
”来获得简单的帮助屏幕
示例
在MarkupScript
项目的scripts子文件夹中有一些示例脚本。将它们作为语言参考;我已尽可能多地使用和注释。SimpleGame.xaml仅展示了基础知识。只需启动它并观看其运行。
TestRunner.xaml和TestClient.xaml展示了更高级的用法。它们是我的集成测试参考。只有TestRunner
打算直接启动,而它内部会启动TestClient
并带有一些参数,并检查其输出。
如果您以前从未运行过它,您需要使用/recording
开关调用它,这样它就可以收集TestClient
的输出并存储起来,以便与后续运行进行比较。
试试看,从这个开始,看看集成的帮助屏幕
MarkupScript scripts\TestRunner.xaml /?
然后运行
MarkupScript scripts\TestRunner.xaml /Recording
现在您已经准备好测试环境,可以开始测试本身了
MarkupScript scripts\TestRunner.xaml
历史
- V1.00 初始版本