2011-11-28 73 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打電話給我時,我的存根都會打印到調試控制檯 - 並進行縮進 - 並顯示插入實際掛鉤的最佳位置/時間。 –