Подтвердить что ты не робот

Как создать расширение разметки XAML, которое возвращает коллекцию

Я использую сериализацию 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>

Свойство Языки для каждого объекта Страна должно быть заполнено с помощью 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> , заполненный ссылочными элементами. Это работает, потому что обратные ссылки на экземпляры Language разрешаются следующей строкой кода:

var token = service.Resolve(item);

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

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

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

Кто-нибудь может предложить, как лучше всего это сделать?

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

4b9b3361

Ответ 1

Вы не можете использовать методы 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; }
    }
}

Ответ 2

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

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

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

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

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

IMHO, это изменение сделало 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
        }
    };
}

[edit...]

Поскольку я просто изучаю службы 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;
        }
    };
}