如何创建返回集合的XAML标记扩展

我正在使用XAML序列化作为对象图(在WPF / Silverlight之外),我正在尝试创建一个自定义标记扩展,它允许使用对XAML中其他地方定义的集合的选定成员的引用来填充集合属性。

这是一个简化的XAML片段,演示了我的目标:

               

每个Country对象的Languages属性将使用IEnumerable 填充,其中包含对LanguageSelector中指定的Language对象的引用,该对象是自定义标记扩展。

以下是我尝试创建将在此角色中使用的自定义标记扩展:

 [ContentProperty("Items")] [MarkupExtensionReturnType(typeof(IEnumerable))] public class LanguageSelector : MarkupExtension { public LanguageSelector(string items) { Items = items; } [ConstructorArgument("items")] public string Items { get; set; } public override object ProvideValue(IServiceProvider serviceProvider) { var service = serviceProvider.GetService(typeof(IXamlNameResolver)) as IXamlNameResolver; var result = new Collection(); foreach (var item in Items.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(item => item.Trim())) { var token = service.Resolve(item); if (token == null) { var names = new[] { item }; token = service.GetFixupToken(names, true); } if (token is Language) { result.Add(token as Language); } } return result; } } 

实际上,这段代码几乎可以工作。 只要引用的对象在引用它们的对象之前在XAML中声明, ProvideValue方法就会正确返回用引用的项填充的IEnumerable 。 这是有效的,因为语言实例的后向引用由以下代码行解析:

 var token = service.Resolve(item); 

但是,如果XAML包含前向引用(因为语言对象是在Country对象之后声明的),它会中断,因为这需要修复标记(显然)不能强制转换为语言

 if (token == null) { var names = new[] { item }; token = service.GetFixupToken(names, true); } 

作为一个实验,我尝试将返回的集合转换为Collection ,希望XAML以某种方式稍后解析令牌,但在反序列化期间抛出无效的强制转换exception。

任何人都可以建议如何最好地使这个工作?

非常感谢,蒂姆

您不能使用GetFixupToken方法,因为它们返回的内部类型只能由在默认XAML架构上下文下工作的现有XAML编写器处理。

但您可以使用以下方法:

 [ContentProperty("Items")] [MarkupExtensionReturnType(typeof(IEnumerable))] public class LanguageSelector : MarkupExtension { public LanguageSelector(string items) { Items = items; } [ConstructorArgument("items")] public string Items { get; set; } public override object ProvideValue(IServiceProvider serviceProvider) { string[] items = Items.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); return new IEnumerableWrapper(items, serviceProvider); } class IEnumerableWrapper : IEnumerable, IEnumerator { string[] items; IServiceProvider serviceProvider; public IEnumerableWrapper(string[] items, IServiceProvider serviceProvider) { this.items = items; this.serviceProvider = serviceProvider; } public IEnumerator GetEnumerator() { return this; } int position = -1; public Language Current { get { string name = items[position]; // TODO use any possible methods to resolve object by name var rootProvider = serviceProvider.GetService(typeof(IRootObjectProvider)) as IRootObjectProvider var nameScope = NameScope.GetNameScope(rootProvider.RootObject as DependencyObject); return nameScope.FindName(name) as Language; } } public void Dispose() { Reset(); } public bool MoveNext() { return ++position < items.Length; } public void Reset() { position = -1; } object IEnumerator.Current { get { return Current; } } IEnumerator IEnumerable.GetEnumerator() { return this; } } } 

这是一个完整且有效的项目,可以解决您的问题。 起初我建议在Country类上使用[XamlSetMarkupExtension]属性,但实际上你需要的只是XamlSchemaContext的前向名称解析。

尽管该function的文档在实际上非常薄,但实际上您可以告诉Xaml Services推迟目标元素,以下代码显示了如何实现。 请注意,即使示例中的部分相反,也可以正确解析所有语言名称。

基本上,如果您需要一个无法解析的名称,请通过返回修正令牌来请求延迟。 是的,正如德米特里提到的那样,它对我们来说是不透明的,但这并不重要。 当您调用GetFixupToken(...) ,您将指定所需的名称列表。 您的标记扩展名 – ProvideValue ,即将在这些名称可用时再次调用。 那时,它基本上是一个重建。

此处未显示您还应检查IsFixupTokenAvailable上的Boolean属性IXamlNameResolver 。 如果以后真的要找到这些名称,那么这应该返回true 。 如果值为false并且您仍然有未解析的名称,那么您应该使操作难以理解,可能是因为Xaml中给出的名称最终无法解析。

有些人可能会好奇地注意到这个项目不是 WPF应用程序,即它不引用WPF库; 您必须添加到此独立ConsoleApplication的唯一参考是System.Xaml 。 即使System.Windows.Markup (历史工件)有一个using语句,也是如此。 在.NET 4.0中,XAML服务支持从WPF(和其他地方)移动到核心BCL库中。

恕我直言,这一变化使XAML服务成为没有人听说过的最好的BCLfunction。 没有更好的基础来开发具有根本重新配置function作为主要要求的大型系统级应用程序。 这种“app”的一个例子是WPF。

 using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Windows.Markup; using System.Xaml; namespace test { public class Language { } public class Country { public IEnumerable Languages { get; set; } } public class LanguageSelector : MarkupExtension { public LanguageSelector(String items) { this.items = items; } String items; public override Object ProvideValue(IServiceProvider ctx) { var xnr = ctx.GetService(typeof(IXamlNameResolver)) as IXamlNameResolver; var tmp = items.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) .Select(s_lang => new { s_lang, lang = xnr.Resolve(s_lang) as Language }); var err = tmp.Where(a => a.lang == null).Select(a => a.s_lang); return err.Any() ? xnr.GetFixupToken(err) : tmp.Select(a => a.lang).ToList(); } }; public class myClass { Collection _l = new Collection(); public Collection Languages { get { return _l; } } Collection _c = new Collection(); public Collection Countries { get { return _c; } } // you must set the name of your assembly here ---v const string s_xaml = @"              "; static void Main(string[] args) { var xxr = new XamlXmlReader(new StringReader(s_xaml)); var xow = new XamlObjectWriter(new XamlSchemaContext()); XamlServices.Transform(xxr, xow); myClass mc = (myClass)xow.Result; /// works with forward references in Xaml } }; } 

[编辑…]

由于我刚学习XAML服务 ,我可能一直在思考它。 下面是一个简单的解决方案,它允许您建立所需的任何引用 – 完全在XAML中 – 仅使用内置标记扩展x:Arrayx:Reference

不知怎的,我没有意识到x:Reference不仅可以填充一个属性(因为它常见: {x:Reference some_name} ),但它也可以自己作为XAML标记( )。 在任何一种情况下,它都充当对文档中其他对象的代理引用。 这允许您使用对其他XAML对象的引用来填充x:Array ,然后只需将该数组设置为您的属性的值。 XAML解析器根据需要自动解析转发引用。

                                        

要试用它,这是一个完整的控制台应用程序,它从前面的XAML文件中实例化myClass对象。 和以前一样,添加对System.Xaml.dll的引用并更改上面的XAML的第一行以匹配您的程序集名称。

 using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Xaml; namespace test { public class Language { } public class Country { public IEnumerable Languages { get; set; } } public class myClass { Collection _l = new Collection(); public Collection Languages { get { return _l; } } Collection _c = new Collection(); public Collection Countries { get { return _c; } } static void Main() { var xxr = new XamlXmlReader(new StreamReader("XMLFile1.xml")); var xow = new XamlObjectWriter(new XamlSchemaContext()); XamlServices.Transform(xxr, xow); myClass mc = (myClass)xow.Result; } }; }