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

e45ebc9d18ff429db5a9d4890b2481e7.pngХочу рассказать, как максимально удобно работать с XAML-ресурсами из Code-Behind. В этой статье мы разберемся, как работают пространства имен XAML, узнаем о XmlnsDefinitionAttribute, используем Т4-шаблоны и сгенерируем статичный класс для доступа к XAML-ресурсам.

ВведениеПри работе с XAML широко используется ResourceDictionary для организации ресурсов: стилей, кистей, конвертеров. Рассмотрим ресурс, объявленный в App.xaml: При верстке View этот ресурс будет использоваться таким образом: Когда необходимо использовать тот же самый ресурс из Code-Behind, обычно применяется конструкция: header.Foreground = (SolidColorBrush)Application.Current.Resources[«HeaderBrush»]; В ней есть ряд недостатков: строковой идентификатор (ключ) ресурса увеличивает вероятность ошибки, а при большом количестве ресурсов, скорее всего, придется лезть в xaml и вспоминать этот самый ключ. Еще одна неприятная мелочь — приведение к SolidColorBrush т.к. все ресурсы хранятся в виде object.Эти недостатки могут быть устранены с помощью кодогенерации, в конечном счете получится такая конструкция:

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

9f11f816f4b94c20b38cda1fdf26b01e.png

Если вы не очень знакомы с 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 будет объявлен так:

В этом случае XAML-анализатор при разборе документа смотрит на префикс local, который описывает, где искать данный тип. xmlns: local=«clr-namespace: CustomNamespace» Зарезервированное имя атрибута — xmlns — указывает на то, что это объявление пространства имен XML. Имя префикса (в данном случае «local») может быть любым в рамках правил XML-разметки. А также оно вообще может отсутствовать, тогда объявление пространства имен принимает вид: xmlns=«clr-namespace: CustomNamespace» Такая запись устанавливает пространство имен по умолчанию для элементов, объявленных без префиксов. Если, например, пространство имен CustomNamespace будет объявлено по умолчанию, то CustomControl можно будет использовать без префикса: В приведенном выше примере, значение атрибута xmlns содержит метку clr-namespace, сразу за которой следует указание на пространство имен .net. Благодаря этому XAML-анализатор понимает, что ему нужно искать CustomControl в пространстве имен CustomNamespace.Типы, входящие в состав SDK, например, SolidColorBrush объявляются без префикса.

Это возможно благодаря тому, что в корневом элементе 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 возможна такая запись:

Все вышеизложенное понадобится для создания скрипта, который может правильно понять 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> _registeredNamespaces = new List>();

public XmlnsAliasResolver (VSProject project, string alias) { foreach (var reference in project.References.OfType() .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:

public Dictionary TypeResolvers (XmlDocument xmlDocument) { var resolvers = new Dictionary(); var namespaces = xmlDocument.SelectNodes (»//namespace::*»).OfType().Distinct ().ToArray ();

foreach (var nmsp in namespaces) { var match = Regex.Match (string.Format (»{0}=\»{1}\», nmsp.Name, nmsp.Value), @«xmlns:(? \w*)=»((clr-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().ToArray ();

var result = new List();

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().ToArray ();

var result = new List();

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 TypeResolvers (XmlDocument xmlDocument) { var resolvers = new Dictionary(); var namespaces = xmlDocument.SelectNodes (»//namespace::*»).OfType().Distinct ().ToArray ();

foreach (var nmsp in namespaces) { var match = Regex.Match (string.Format (»{0}=\»{1}\», nmsp.Name, nmsp.Value), @«xmlns:(? \w*)=»((clr-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> _registeredNamespaces = new List>();

public XmlnsAliasResolver (VSProject project, string alias) { foreach (var reference in project.References.OfType() .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]; } #>

© Habrahabr.ru