[Из песочницы] Как получить удобный доступ к XAML-ресурсам из Code-Behind
Хочу рассказать, как максимально удобно работать с XAML-ресурсами из Code-Behind. В этой статье мы разберемся, как работают пространства имен XAML, узнаем о XmlnsDefinitionAttribute, используем Т4-шаблоны и сгенерируем статичный класс для доступа к XAML-ресурсам.
ВведениеПри работе с XAML широко используется ResourceDictionary для организации ресурсов: стилей, кистей, конвертеров. Рассмотрим ресурс, объявленный в App.xaml:
header.Foreground = AppResources.HeaderBrush; Сразу оговорюсь, что поскольку цель статьи — показать сам подход, для упрощения я заостряю внимание на одном файле App.xaml, но при желании несложные модификации позволят обработать все XAML-ресурсы в проекте и даже разложить их в отдельные файлы.Создаем T4-шаблон:
Если вы не очень знакомы с 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 будет объявлен так:
[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 возможна такая запись:
public string ResolveTypeFullName (string localTagName) { return _singleNamespace + ».» + localTagName; } } Данная реализация обрабатывает случай, когда .net пространство имен указано явно с использованием clr-namespace.Другой за случай отвечает XmlnsAliasResolver:
public class XmlnsAliasResolver: ITypeResolver
{
private readonly List
public XmlnsAliasResolver (VSProject project, string alias)
{
foreach (var reference in project.References.OfType
_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
foreach (var nmsp in namespaces)
{
var match = Regex.Match (string.Format (»{0}=\»{1}\», nmsp.Name, nmsp.Value),
@«xmlns:(?
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
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
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
foreach (var nmsp in namespaces)
{
var match = Regex.Match (string.Format (»{0}=\»{1}\», nmsp.Name, nmsp.Value),
@«xmlns:(?
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
public XmlnsAliasResolver (VSProject project, string alias)
{
foreach (var reference in project.References.OfType
_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]; } #>