Поиск на сайте: Расширенный поиск


Новые программы oszone.net Читать ленту новостей RSS
EF StartUp Manager - программа для слежением за программами, которые запускаются вместе с загрузкой операционной системы...
EF Mailbox Manager - программа для периодической проверки неограниченного количество аккаунтов электронной почты. Вы мож...
Программа позволяет узнать, чем занято место на жестком диске. Сканирует папки очень быстро и выводит наглядную диаграмм...
HashMyFiles - это маленькая утилита, которая предоставляет возможность рассчета контрольных сумм MD5 и SHA1 для одного и...
Файловый менеджер с очень малыми системными требованиями, но тем не менее с большими возможностями. Программа имеет ориг...
OSzone.net Microsoft Разработка приложений Другое Как получить удобный доступ к XAML-ресурсам из Code-Behind RSS

Как получить удобный доступ к XAML-ресурсам из Code-Behind

Текущий рейтинг: 0 (проголосовало 0)
 Посетителей: 724 | Просмотров: 1598 (сегодня 0)  Шрифт: - +
В этой статье мы разберемся, как работают пространства имен XAML, узнаем о XmlnsDefinitionAttribute, используем Т4-шаблоны и сгенерируем статичный класс для доступа к XAML-ресурсам.

Введение

При работе с XAML широко используется ResourceDictionary для организации ресурсов: стилей, кистей, конвертеров. Рассмотрим ресурс, объявленный в App.xaml:

<Application.Resources>
    <SolidColorBrush x:Key="HeaderBrush" Color="Black" />
<Application.Resources>

При верстке View этот ресурс будет использоваться таким образом:

<TextBlock x:Name="header" Foreground="{StaticResource HeaderBrush}" />

Когда необходимо использовать тот же самый ресурс из Code-Behind, обычно применяется конструкция:

header.Foreground = (SolidColorBrush)Application.Current.Resources["HeaderBrush"];

В ней есть ряд недостатков: строковой идентификатор (ключ) ресурса увеличивает вероятность ошибки, а при большом количестве ресурсов, скорее всего, придется лезть в xaml и вспоминать этот самый ключ. Еще одна неприятная мелочь — приведение к SolidColorBrush т.к. все ресурсы хранятся в виде object.

Эти недостатки могут быть устранены с помощью кодогенерации, в конечном счете получится такая конструкция:

header.Foreground = AppResources.HeaderBrush;

Сразу оговорюсь, что поскольку цель статьи — показать сам подход, для упрощения я заостряю внимание на одном файле App.xaml, но при желании несложные модификации позволят обработать все XAML-ресурсы в проекте и даже разложить их в отдельные файлы.

Создаем T4-шаблон:



Используем стандартный для T4-заголовок:

<#@ template debug="false" hostSpecific="true" language="C#" #>
<#@ output extension=".cs" #>

Установка hostSpecific=true необходима для того, чтобы иметь доступ к свойству Host класса TextTransformation, от которого наследуется класс шаблона T4. С помощью Host будет осуществляться доступ к файловой структуре проекта и к некоторым другим необходимым данным.

Все ресурсы будут собраны в один статичный класс со статичными readonly Property. Основной скелет шаблона выглядит так:

using System.Windows;
namespace <#=ProjectDefaultNamespace#>
{
    public static class AppResources
    {
<#
        foreach (var resource in ResourcesFromFile("/App.xaml"))
        {
            OutputPropery(resource);
        }
#>
    }
}

Все вспомогательные функции и свойства, задействованные в скрипте, объявляются в секции <#+ #> после основного тела скрипта.

Первое свойство VsProject выбирает проект из Solution, в котором лежит сам скрипт:

private VSProject _vsProject;
public VSProject VSProject
{
    get
    {
        if (_vsProject == null)
        {
            var serviceProvider = (IServiceProvider) Host;
            var dte = (DTE)serviceProvider.GetService(typeof (DTE));
            _vsProject = (VSProject)dte.Solution.FindProjectItem(Host.TemplateFile).ContainingProject.Object;
        }
        return _vsProject;
    }
}

ProjectDefaultNamespace — пространство имен проекта:

private string _projectDefaultNamespace;
public string ProjectDefaultNamespace
{
    get
    {
        if (_projectDefaultNamespace == null)
            _projectDefaultNamespace = VSProject.Project.Properties.Item("DefaultNamespace").Value.ToString();

        return _projectDefaultNamespace;
    }
}

Всю основную работу по сбору ресурсов из XAML выполняет ResourcesFromFile(string filename). Чтобы понять принцип его работы, разберем подробней, как в XAML устроены пространства имен, префиксы, а также как они используются.

Пространства имен и префиксы в XAML

Чтобы однозначно указать на определенный тип в C#, необходимо полностью указать имя типа вместе с пространством имен, в котором он объявлен:

var control = new CustomNamespace.CustomControl();

При использовании using приведенную выше конструкцию можно записать короче:

using CustomNamespace;
var control = new CustomControl();

Похожим образом работают и пространства имен в XAML. XAML — это подмножество XML и использует правила объявления пространств имен из XML.

Тип CustomControl в XAML будет объявлен так:

<local:CustomControl />

В этом случае XAML-анализатор при разборе документа смотрит на префикс local, который описывает, где искать данный тип.

xmlns:local="clr-namespace:CustomNamespace"

Зарезервированное имя атрибута — xmlns — указывает на то, что это объявление пространства имен XML. Имя префикса (в данном случае “local”) может быть любым в рамках правил XML-разметки. А также оно вообще может отсутствовать, тогда объявление пространства имен принимает вид:

xmlns="clr-namespace:CustomNamespace"

Такая запись устанавливает пространство имен по умолчанию для элементов, объявленных без префиксов. Если, например, пространство имен CustomNamespace будет объявлено по умолчанию, то CustomControl можно будет использовать без префикса:

<CustomControl />

В приведенном выше примере, значение атрибута xmlns содержит метку clr-namespace, сразу за которой следует указание на пространство имен .net. Благодаря этому XAML-анализатор понимает, что ему нужно искать CustomControl в пространстве именCustomNamespace.

Типы, входящие в состав SDK, например, SolidColorBrush объявляются без префикса.

<SolidColorBrush Color="Red" />

Это возможно благодаря тому, что в корневом элементе XAML-документа объявлено пространство имен по умолчанию:

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

Это второй способ объявления пространства имен в XAML. Значение атрибута xmlns — некоторая уникальная строка-alias, она не содержит clr-namespace. Когда XAML-анализатор встречает такую запись, он проверяет .net сборки проекта на атрибутXmlnsDefinitionAttribute.

Атрибут XmlnsDefinitionAttribute переменяется к сборке множество раз описывая пространства имен соответствующие alias-строке:

[assembly: XmlnsDefinition("http://schemas.microsoft.com/winfx/2006/xaml/presentation", "System.Windows")]
[assembly: XmlnsDefinition("http://schemas.microsoft.com/winfx/2006/xaml/presentation", "System.Windows.Media")]
[assembly: XmlnsDefinition("http://schemas.microsoft.com/winfx/2006/xaml/presentation", "System.Windows.Shapes")]

Сборка System.WIndows помечена множеством таких атрибутов, таким образом alias schemas.microsoft.com/winfx/2006/xaml/presentation включает в себя множество пространств имен из стандартной SDK таких как: System.Windows, System.Windows.Media и т.д. Это позволяет сопоставить пространство имен XML множеству пространств имен из .net.

Стоит заметить, что, если в двух пространствах имен, объединенных под одним alias, есть типы с одинаковым именем, то возникнет коллизия, и XAML-анализатор не сможет разобрать, откуда ему взять искомый тип.

Итак, теперь мы знаем, что пространства имен XAML сопоставляются с пространствами имен в .net двумя разными способами: один к одному при использовании clr-namespace и один ко многим при использовании alias.

Конструкция xmlns, как правило, встречается в корневом элементе XAML-документа, но на самом деле достаточно, чтобы xmlns был объявлен хотя бы на том же уровне, на котором используется. В случае с CustomControl возможна такая запись:

<local:CustomControl xmlns:local="clr-namespace:CustomNamespace"  />

Все вышеизложенное понадобится для создания скрипта, который может правильно понять XAML-разметку ReosurceDictionary в котором могут лежать разнородные объекты, входящие в SDK, а также компоненты сторонних библиотек, использующих разные способы объявления пространств имен.

Приступим к основной части

Задача по определению полного имени типа по XAML-тегу возложена на интерфейс ITypeResolver:

public interface ITypeResolver
{
    string ResolveTypeFullName(string localTagName);
}

Поскольку есть два вида объявления пространства имен, получилось две реализации данного интерфейса:

public class ExplicitNamespaceResolver : ITypeResolver
{
    private string _singleNamespace;
    public ExplicitNamespaceResolver(string singleNamespace)
    {
        _singleNamespace = singleNamespace;
    }

    public string ResolveTypeFullName(string localTagName)
    {
        return _singleNamespace + "." + localTagName;
    }
}

Данная реализация обрабатывает случай, когда .net пространство имен указано явно с использованием clr-namespace.

Другой за случай отвечает XmlnsAliasResolver:

public class XmlnsAliasResolver : ITypeResolver
{
    private readonly List<Tuple<string, Assembly>> _registeredNamespaces = new List<Tuple<string, Assembly>>();

    public XmlnsAliasResolver(VSProject project, string alias)
    {
        foreach (var reference in project.References.OfType<Reference>()
            .Where(r => r.Path.EndsWith(".dll", StringComparison.InvariantCultureIgnoreCase)))
        {
            try
            {
                var assembly = Assembly.ReflectionOnlyLoadFrom(reference.Path);

                _registeredNamespaces.AddRange(assembly.GetCustomAttributesData()
                    .Where(attr => attr.AttributeType.Name == "XmlnsDefinitionAttribute" &&
                                   attr.ConstructorArguments[0].Value.Equals(alias))
                    .Select(attr => Tuple.Create(attr.ConstructorArguments[1].Value.ToString(), assembly)));
            }
            catch {}
        }
    }

    public string ResolveTypeFullName(string localTagName)
    {
        return _registeredNamespaces.Select(i => i.Item2.GetType(i.Item1 + "." + localTagName)).First(i => i != null).FullName;
    }
}

XmlnsAliasResolver регистрирует внутри себя пространства имен, помеченные атрибутом XmlnsDefinitionAttribute с определенным alias, и сборки, в которых они объявлены. Поиск осуществляется в каждом зарегистрированном пространстве имен, пока не будет найден результат.

В реализацию ResolveTypeFullName по желанию можно добавить кэширование найденных типов.

Вспомогательный метод TypeResolvers разбирает XAML-документ, находит все пространства имен и сопоставляет их XML-префиксу, на выходе получается “словарь” Dictionary<string, ITypeResolver>:

public Dictionary<string, ITypeResolver> TypeResolvers(XmlDocument xmlDocument)
{
    var resolvers = new Dictionary<string, ITypeResolver>();
    var namespaces = xmlDocument.SelectNodes("//namespace::*").OfType<XmlNode>().Distinct().ToArray();

    foreach (var nmsp in namespaces)
    {
        var match = Regex.Match(string.Format("{0}=\"{1}\"", nmsp.Name, nmsp.Value),
            @"xmlns:(?<prefix>\w*)=""((clr-namespace:(?<namespace>[\w.]*))|([^""]))*""");

        var namespaceGroup = match.Groups["namespace"];
        var prefix = match.Groups["prefix"].Value;

        if (string.IsNullOrEmpty(prefix))
            prefix = "";

        if (resolvers.ContainsKey(prefix))
            continue;

        if (namespaceGroup != null && namespaceGroup.Success)
        {
            //Явное указание namespace
            resolvers.Add(prefix, new ExplicitNamespaceResolver(namespaceGroup.Value));
        }
        else
        {
            //Alias который указан в XmlnsDefinitionAttribute
            resolvers.Add(prefix, new XmlnsAliasResolver(VSProject, nmsp.Value));
        }
    }

    return resolvers;
}

С помощью xpath — "//namespace::*" выбираются все пространства имен, объявленные на любых уровнях документа. Далее каждое пространство имен разбирается регулярным выражением на префикс и на пространство имен .net, указаное после clr-namespace, если оно есть. В соответствие с результатами создается либо ExplicitNamespaceResolver, либо XmlnsAliasResolver и сопоставляется с префиксом или префиксом по умолчанию.

Метод ResourcesFromFile собирает все воедино:

public Resource[] ResourcesFromFile(string filename)
{
    var xmlDocument = new XmlDocument();
    xmlDocument.Load(Path.GetDirectoryName(VSProject.Project.FileName) + filename);

    var typeResolvers = TypeResolvers(xmlDocument);

    var nsmgr = new XmlNamespaceManager(xmlDocument.NameTable);
    nsmgr.AddNamespace("x", "http://schemas.microsoft.com/winfx/2006/xaml");

    var resourceNodes = xmlDocument.SelectNodes("//*[@x:Key]", nsmgr).OfType<XmlNode>().ToArray();

    var result = new List<Resource>();

    foreach (var resourceNode in resourceNodes)
    {
        var prefix = GetPrefix(resourceNode.Name);
        var localName = GetLocalName(resourceNode.Name);

        var resourceName = resourceNode.SelectSingleNode("./@x:Key", nsmgr).Value;

        result.Add(new Resource(resourceName, typeResolvers[prefix].ResolveTypeFullName(localName)));
    }

    return result.ToArray();
}

После загрузки XAML-докуменета и инициализации typeResolvers для правильной работы xpath в XmlNamespaceManager добавляется пространство имен schemas.microsoft.com/winfx/2006/xaml, на которое указывают все атрибуты-ключи в ResourceDictionary.

При использовании xpath — "//*[@x:Key]" со всех уровней XAML документа выбираются объекты имеющие атрибут-ключ. Далее скрипт пробегает по всем найденным объектам и с помощью “словаря” typeResolvers ставит в соответствие каждому полное имя .net типа.

На выходе получается массив структур Resource, содержащих в себе все необходимые данные для кодогенерации:

public struct Resource
{
    public string Key { get; private set; }
    public string Type { get; private set; }

    public Resource(string key, string type) : this()
    {
        Key = key;
        Type = type;
    }
}

Ну и напоследок метод, который выводит полученный Resource в виде текста:

public void OutputPropery(Resource resource)
{
#>
        private static bool _<#=resource.Key #>IsLoaded;
        private static <#=resource.Type #> _<#=resource.Key #>;
        public static <#=resource.Type #> <#=resource.Key #>
        {
            get
            {
                if (!_<#=resource.Key #>IsLoaded)
                {
                    _<#=resource.Key #> = (<#=resource.Type #>)Application.Current.Resources["<#=resource.Key #>"];
                    _<#=resource.Key #>IsLoaded = true;
                }
                return _<#=resource.Key #>;
            }
        }
<#+
}

Стоит заметить, что свойство Key возвращает значение атрибута-ключа из XAML как есть, и случаи использования ключей с символами, не валидными для объявления свойств в C#, приведут к ошибке. Дабы не усложнять и без того большие куски кода, я намеренно оставляю реализацию получения безопасных для Property имен на ваше усмотрение.

Заключение

Данный скрипт работает в WPF-, Silverlight-, WindowsPhone-проектах. Что касается семейства WindowsRT, UniversalApps.

Автор: Найденов Юрий  •  Иcточник: msdn.microsoft.com  •  Опубликована: 15.05.2015
Нашли ошибку в тексте? Сообщите о ней автору: выделите мышкой и нажмите CTRL + ENTER
Теги:   XAML.


Оценить статью:
Вверх
Комментарии посетителей RSS

Чтобы оставить комментарий, зарегистрируйтесь или войдите с учетной записью социальной сети.