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

в 13:32, , рубрики: .net, silverlight, windows phone, wpf, кодогенерация, разработка под windows phone

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

Хочу рассказать, как максимально удобно работать с XAML-ресурсами из Code-Behind. В этой статье мы разберемся, как работают пространства имен 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-шаблон:

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

Если вы не очень знакомы с 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, в следующих статьях мы окунемся в XamlTypeInfo.g.cs, поговорим о IXamlMetadataProvider, который пришел на смену XmlnsDefinitionAttribute и заставим скрипт работать с UniversalApps.

Под спойлером вы можете найти полный код скрипта, копируйте в свой проект, используйте с удовольствием.

Полный код скрипта

<#@ template debug="false" hostSpecific="true" language="C#" #>
<#@ assembly name="System.Windows" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="System.Linq" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="System.Xml" #>
<#@ assembly name="System.Xml.Linq" #>
<#@ assembly name="EnvDTE" #>
<#@ assembly name="VSLangProj" #>
<#@ import namespace="EnvDTE" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Reflection" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Xml" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ import namespace="VSLangProj" #>
<#@ output extension=".cs" #>
using System.Windows;

namespace <#=ProjectDefaultNamespace#>
{
    public static class AppResourcess
    {
<#
		foreach (var resource in ResourcesFromFile("/App.xaml"))
		{
			OutputPropery(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 #>;
			}
		}
<#+
    }

    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;
        }
    }

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

            return _projectDefaultNamespace;
        }
    }

	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;
	    }
	}

    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();
    }

    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;
    }

    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;
        }
    }

    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;
        }
    }
  
    string GetPrefix(string xamlTag)
    {
        if (string.IsNullOrEmpty(xamlTag))
            throw new ArgumentException("xamlTag is null or empty", "xamlTag");

        var strings = xamlTag.Split(new[] {":"}, StringSplitOptions.RemoveEmptyEntries);

		if(strings.Length <2)
		    return "";

        return strings[0];
    } 
	
	string GetLocalName(string xamlTag)
    {
        if (string.IsNullOrEmpty(xamlTag))
            throw new ArgumentException("xamlTag is null or empty", "xamlTag");

        var strings = xamlTag.Split(new[] {":"}, StringSplitOptions.RemoveEmptyEntries);

		if(strings.Length <2)
		    return xamlTag;

        return strings[1];
    }
#>

Автор: EBCEu4

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js