14

オブジェクトグラフ(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>

各CountryオブジェクトのLanguagesプロパティには、カスタムマークアップ拡張機能であるLanguageSelectorで指定されたLanguageオブジェクトへの参照を含むIEnumerable<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<Language>を正しく返します。これが機能するのは、Languageインスタンスへの後方参照が次のコード行によって解決されるためです。

var token = service.Resolve(item);

ただし、XAMLに前方参照が含まれている場合(LanguageオブジェクトはCountryオブジェクトの後に宣言されているため)、これには(明らかに) Languageにキャストできない修正トークンが必要なために壊れます。

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

実験として、XAMLが後でトークンを何らかの方法で解決することを期待して、返されたコレクションをCollection <object>に変換しようとしましたが、逆シリアル化中に無効なキャスト例外がスローされます。

誰かがこれを機能させるための最善の方法を提案できますか?

どうもありがとう、ティム

4

2 に答える 2

15

これは、問題を解決する完全で機能するプロジェクトです。[XamlSetMarkupExtension]最初は、クラスで属性を使用することを提案するつもりでしたCountryが、実際に必要なのはXamlSchemaContextの前方名前解決だけです。

この機能のドキュメントは非常に薄いものです、実際にはXaml Servicesにターゲット要素を延期するように指示することができます。次のコードはその方法を示しています。例のセクションが逆になっていても、すべての言語名が適切に解決されることに注意してください。

基本的に、解決できなかった名前が必要な場合は、フィックスアップ トークンを返すことで保留を要求します。はい、ドミトリーが言及しているように、それは私たちにとって不透明ですが、それは問題ではありません. を呼び出すときにGetFixupToken(...)、必要な名前のリストを指定します。マークアップ拡張機能 (<code>ProvideValue) は、これらの名前が使用可能になったときに再度呼び出されます。その時点で、それは基本的にやり直しです。

ここでは示されていませんが、 のプロパティも確認する必要がありBooleanます。名前が本当に後で見つかる場合、これは を返す必要があります。値がで、まだ未解決の名前がある場合は、おそらく Xaml で指定された名前を最終的に解決できなかったため、操作をハード フェイルする必要があります。IsFixupTokenAvailableIXamlNameResolvertruefalse

このプロジェクトはWPF アプリではないことに注意してください。つまり、WPF ライブラリを参照していません。このスタンドアロンのConsoleApplicationに追加する必要がある唯一の参照は ですSystem.Xaml。これは、 (歴史的遺物)usingに関する記述があっても当てはまります。System.Windows.MarkupXAML サービスのサポートが WPF (およびその他の場所) からコア BCL ライブラリに移動されたのは、.NET 4.0 でした。

私見ですが、この変更により、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 Servicesを学んでいるので、考えすぎていたのかもしれません。以下は、組み込みのマークアップ拡張機能x:Arrayx:Reference.

どういうわけかx:Reference、属性を設定できるだけでなく (一般的に見られるように: {x:Reference some_name})、それ自体が XAML タグとしても機能する ( <Reference Name="some_name" />) ことに気付きませんでした。いずれの場合も、ドキュメント内の別の場所にあるオブジェクトへのプロキシ参照として機能します。これにより、x:Arrayに他の XAML オブジェクトへの参照を設定し、その配列をプロパティの値として設定するだけです。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>

これを試すためにmyClass、前の XAML ファイルからオブジェクトをインスタンス化する完全なコンソール アプリを次に示します。前と同様に、参照を追加し、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;
        }
    };
}
于 2012-09-14T04:09:26.897 に答える
7

GetFixupTokenメソッドは、既定の XAML スキーマ コンテキストで動作する既存の XAML ライターによってのみ処理できる内部型を返すため、使用できません。

ただし、代わりに次のアプローチを使用できます。

[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; }
    }
}
于 2011-11-29T08:26:04.867 に答える