Как создать расширение разметки XAML, которое возвращает коллекцию - PullRequest
13 голосов
/ 29 ноября 2011

Я использую сериализацию XAML для графа объектов (вне WPF / Silverlight) и пытаюсь создать собственное расширение разметки, которое позволит заполнять свойство коллекции, используя ссылки на выбранные члены коллекции, определенные в другом месте в 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>

Свойство Languages ​​ каждого объекта Country предназначено длязаполняться IEnumerable , содержащим ссылки на Language объекты, указанные в LanguageSelector , который является пользовательским расширением разметки.

Вот моя попытка создать собственное расширение разметки, которое будет выполнять эту роль:

[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 разрешаются следующей строкой кода:

var token = service.Resolve(item);

Но, если XAML содержит прямые ссылки (потому что Language объекты объявляются после объектов Country ), он ломается, потому что для этого требуются токены исправления, которые (очевидно) не могут быть преобразованы в Language .

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

КакВ эксперименте я попытался преобразовать возвращаемую коллекцию в Collection в надежде, что XAML каким-то образом разрешит токены позже, но при десериализации будет выдано недопустимое исключение приведения.чтобы это заработало?

Большое спасибо, Тим

Ответы [ 2 ]

13 голосов
/ 14 сентября 2012

Вот полный и рабочий проект, который решает вашу проблему. Сначала я собирался предложить использовать атрибут [XamlSetMarkupExtension] в вашем классе Country, но на самом деле все, что вам нужно, - это прямое разрешение имен XamlSchemaContext.

Хотя документация для этой функции очень тонкая на местах, вы можете фактически сказать Xaml Services отложить ваш целевой элемент, и следующий код показывает, как. Обратите внимание, что все названия ваших языков правильно разрешены, даже если разделы из вашего примера поменялись местами.

Обычно, если вам нужно имя, которое не может быть разрешено, вы запрашиваете отсрочку, возвращая токен исправления. Да, как отмечает Дмитрий, это непрозрачно для нас, но это не имеет значения. Когда вы звоните GetFixupToken(...), вы указываете список имен, которые вам нужны. Ваше расширение разметки - ProvideValue, то есть - будет вызвано снова позже, когда эти имена станут доступными. В этот момент это в основном перебор.

Здесь не показано, что вы также должны проверить Boolean свойство IsFixupTokenAvailable на IXamlNameResolver. Если имена действительно будут найдены позже, это должно вернуть true. Если значение равно false и у вас все еще есть неразрешенные имена, то вам следует выполнить полный сбой операции, предположительно потому, что имена, указанные в Xaml, в конечном итоге не могут быть разрешены.

Некоторым может быть любопытно отметить, что этот проект не является приложением WPF, т. Е. Он не ссылается на библиотеки WPF; единственная ссылка, которую вы должны добавить к этому автономному ConsoleApplication - это System.Xaml. Это верно, хотя существует оператор using для System.Windows.Markup (исторический артефакт). Именно в .NET 4.0 поддержка служб XAML была перенесена из WPF (и в других местах) в основные библиотеки BCL.

ИМХО, это изменение сделало XAML Services величайшей функцией 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:Array и x: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;
        }
    };
}
6 голосов
/ 29 ноября 2011

Вы не можете использовать методы 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; }
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...