2011-11-28 71 views
13

我使用XAML序列化的对象图(WPF/Silverlight外部),我试图创建一个自定义标记扩展,将允许使用填充集合属性引用XAML中其他地方定义的集合的选定成员。如何创建一个返回集合的XAML标记扩展

这里是一个简化的XAML片段演示了什么,我的目标是实现:

<myClass.Languages> 
    <LanguagesCollection> 
     <Language x:Name="English" /> 
     <Language x:Name="French" /> 
     <Language x:Name="Italian" /> 
    </LanguagesCollection> 
</myClass.Languages> 

<myClass.Countries> 
    <CountryCollection> 
     <Country x:Name="UK" Languages="{LanguageSelector 'English'}" /> 
     <Country x:Name="France" Languages="{LanguageSelector 'French'}" /> 
     <Country x:Name="Italy" Languages="{LanguageSelector 'Italian'}" /> 
     <Country x:Name="Switzerland" Languages="{LanguageSelector 'English, French, Italian'}" /> 
    </CountryCollection> 
</myClass.Countries> 

每个国家对象的语言属性是与 IEnumerable的<语言>包含要引用填充在 LanguageSelector中指定的对象 Language,这是一个自定义标记扩展。

这是我在创建自定义标记扩展,将这个角色服务的尝试:

[ContentProperty("Items")] 
[MarkupExtensionReturnType(typeof(IEnumerable<Language>))] 
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<Language>(); 

     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包含向前引用(因为语言对象在国家之后宣布对象),它会中断,因为这需要修复令牌(显然)无法投射到语言。

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

作为一个实验我试过,希望XAML会以某种方式解决令牌后返回的集合转换为收藏<对象>,但反序列化过程引发无效转换异常。

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

非常感谢, 添

+0

+1感谢您发表该内容。我发现这是XAML Servces学习曲线的一个很好的练习。我希望下面发布的建议可能在一年后仍然适用于您。 –

+0

@Glenn Slayden:谢谢你对此的跟进。你提出了两个非常创新的解决方案。尽管我的代码现在已经实现并正在使用DmitryG提出的想法,但审查它并使其适用于使用更简洁的方法将会很有趣。 –

回答

6

你不能因为他们返回只能由默认的XAML架构环境下工作中存在的XAML作家要处理的内部类型使用GetFixupToken方法。

但是你可以用下面的办法来代替:

[ContentProperty("Items")] 
[MarkupExtensionReturnType(typeof(IEnumerable<Language>))] 
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<Language>, IEnumerator<Language> { 
     string[] items; 
     IServiceProvider serviceProvider; 
     public IEnumerableWrapper(string[] items, IServiceProvider serviceProvider) { 
      this.items = items; 
      this.serviceProvider = serviceProvider; 
     } 
     public IEnumerator<Language> 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; } 
    } 
} 
+0

非常感谢!这是一个非常聪明的解决方案。 –

+2

德米特里,在这个页面上看到我的答案和工作解决方案;使用'GetFixupToken'没有问题(并且不需要不受支持的编码),但该技术当然没有很好的记录。诀窍是令牌 - 尽管对你来说不透明 - 是为你包含你需要的名字而建立的。什么地方没有提到的是,你然后*从'ProvideValue'方法返回token *。这告诉XAML服务稍后再尝试。 –

+0

@GlennSlayden:Hi Glen,谢谢你的替代解决方案。您提供的信息对我非常感兴趣...(+1 !!!) – DmitryG

12

下面是一个完整和工作项目,解决您的问题。起初,我打算建议在Country课程中使用[XamlSetMarkupExtension]属性,但实际上您只需要XamlSchemaContext的前向名称解析。

尽管该功能的文档,在地面上非常薄,你可以实际上告诉XAML服务推迟你的目标元素,下面的代码演示如何。请注意,即使您的示例中的部分被颠倒过来,您的所有语言名称都可以正确解析。

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

此处未显示的是,您还应该检查IXamlNameResolver上的Boolean属性IsFixupTokenAvailable。如果名字真的在后面找到,那么这应该返回true。如果值为false,并且您仍然有未解析的名称,那么您应该严重失败操作,大概是因为Xaml中给出的名称最终无法解析。

有人可能会好奇地注意到,这个项目是而不是一个WPF应用程序,即它没有引用WPF库;您必须添加到此独立的控制台应用程序的唯一参考是System.Xaml。即使System.Windows.Markup(历史工件)的using声明也是如此。在.NET 4.0中,XAML Services支持已从WPF(以及其他地方)转移到核心BCL库中。

恕我直言,这个变化使XAML服务没有人听说过的最大的BCL功能。开发具有彻底重新配置能力的大型系统级应用程序作为主要需求没有更好的基础。这种'应用'的例子是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<Language> 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<Language> _l = new Collection<Language>(); 
     public Collection<Language> Languages { get { return _l; } } 

     Collection<Country> _c = new Collection<Country>(); 
     public Collection<Country> Countries { get { return _c; } } 

     // you must set the name of your assembly here ---v 
     const string s_xaml = @" 
<myClass xmlns=""clr-namespace:test;assembly=ConsoleApplication2"" 
     xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml""> 

    <myClass.Countries> 
     <Country x:Name=""UK"" Languages=""{LanguageSelector 'English'}"" /> 
     <Country x:Name=""France"" Languages=""{LanguageSelector 'French'}"" /> 
     <Country x:Name=""Italy"" Languages=""{LanguageSelector 'Italian'}"" /> 
     <Country x:Name=""Switzerland"" Languages=""{LanguageSelector 'English, French, Italian'}"" /> 
    </myClass.Countries> 

    <myClass.Languages> 
     <Language x:Name=""English"" /> 
     <Language x:Name=""French"" /> 
     <Language x:Name=""Italian"" /> 
    </myClass.Languages> 

</myClass> 
"; 
     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标记(<Reference Name="some_name" />)。在任何情况下,它都可以作为对文档中其他位置的对象的代理引用。这使您可以使用其他XAML对象的引用来填充x:Array,然后只需将该数组设置为属性的值即可。 XAML解析器根据需要自动解析前向引用。

<myClass xmlns="clr-namespace:test;assembly=ConsoleApplication2" 
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> 
    <myClass.Countries> 
     <Country x:Name="UK"> 
      <Country.Languages> 
       <x:Array Type="Language"> 
        <x:Reference Name="English" /> 
       </x:Array> 
      </Country.Languages> 
     </Country> 
     <Country x:Name="France"> 
      <Country.Languages> 
       <x:Array Type="Language"> 
        <x:Reference Name="French" /> 
       </x:Array> 
      </Country.Languages> 
     </Country> 
     <Country x:Name="Italy"> 
      <Country.Languages> 
       <x:Array Type="Language"> 
        <x:Reference Name="Italian" /> 
       </x:Array> 
      </Country.Languages> 
     </Country> 
     <Country x:Name="Switzerland"> 
      <Country.Languages> 
       <x:Array Type="Language"> 
        <x:Reference Name="English" /> 
        <x:Reference Name="French" /> 
        <x:Reference Name="Italian" /> 
       </x:Array> 
      </Country.Languages> 
     </Country> 
    </myClass.Countries> 
    <myClass.Languages> 
     <Language x:Name="English" /> 
     <Language x:Name="French" /> 
     <Language x:Name="Italian" /> 
    </myClass.Languages> 
</myClass> 

要尝试一下,这里有一个完整的控制台应用程序实例化从前面的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<Language> Languages { get; set; } } 

    public class myClass 
    { 
     Collection<Language> _l = new Collection<Language>(); 
     public Collection<Language> Languages { get { return _l; } } 

     Collection<Country> _c = new Collection<Country>(); 
     public Collection<Country> 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; 
     } 
    }; 
} 
+1

这是一个很好的答案 - 我可以问问您的学习资源是否适用于XAML服务?这是我试图进入自己的东西,但无法找到很多的教程方式,只有MSDN文档可以非常密集 – AlexFoxGill

+3

好问题;现在回想起来,我学习XAML的大部分时间都是在.NET Reflector中花费了无数时间,并检查运行时堆栈跟踪。有一件事从一开始就帮助我们创建了精简的存根/代理类,它们继承了XamlType,XamlMember等的每个函数。幸运的是,XAML服务对这些回调非常慷慨。每当XAML打电话给我时,我的存根都会打印到调试控制台 - 并进行缩进 - 并显示插入实际挂钩的最佳位置/时间。 –