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



Хочу рассказать, как максимально удобно работать с 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-шаблон:



Если вы не очень знакомы с 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];
    }
#>

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 12

    +1
    Ну вообще в MS, очевидно, тоже столкнулись с этой проблемой. Для решения таких ситуаций MS использует не гласный паттерн. Есть такой класс — ComponentResourceKey. Если нужно получить доступ к ресурсу из code-behind, то в классе объявляется статическое поле типа ComponentResourceKey, а в XAML используется конструкция Key={x:Static local:ClassName.FieldName}. Затем доступ к ресурсу производится через Application.Current.FindResource.

    namespace MyNS
    {
        public class MyControl
        {
            public static readonly ComponentResourceKey MyResourceKey =
                new ComponentResourceKey(typeof(MyControl), "MyResource");
    
            private static Brush m_MyResourceCache;
    
            public static Brush MyResource
            {
                get
                {
                     if (null == m_MyResource)
                         m_MyResource = (Brush)Application.Current.FindResource(MyResourceKey);
                     return m_MyResource;
                }
            }
        }
    }
    


    Так сделано, например, в тулбаре. Есть такое поле ToolBar.ButtonStyleKey, соответственно, что бы в XAML сослаться на стиль, можно использовать конструкцию <Button Style={StaticResource {x:Static ToolBar.ButtonStyleKey}}>.
      +1
      На мой взгляд XAML хорош тем, что в нем объявление ресурсов выглядит гармонично.
      Тут получается зеркальная ситуация — мы объявляем ресурсы в коде и ссылаемся на них из XAML.
      К сожалению, расширение разметки x:Static доступно только в WPF.
      0
      1. Классы для работы с XAML включены во фреймворк. Не пробовали читать ими, а не изобретать велосипед?

      2. Вместо VSProject.Project.Properties.Item("DefaultNamespace").Value.ToString() лучше использовать CallContext.LogicalGetData("NamespaceHint"). Эта штука сама просчитает правильный путь по папкам и учтёт неймспейс из настроек скрипта.
        0
        Вы имеете в виду XamlReader? С ним получается вообще трехколесный с reflection.
        По второму пункту согласен. Спасибо за совет.
          –1
          Да ладно?

          Пихаем ресурсы в ResourceDictionary, мержим в App.xaml. А в T4 имеем:

          var res = (ResourceDictionary)XamlReader.Load(File.OpenRead(Host.ResolvePath("Res.xaml")));
          var str = string.Join("\n", res.OfType<DictionaryEntry>().Select(i => string.Format("{0} {1}", i.Value.GetType(), i.Key)));
          

          Плюс перечисление сборок, если в ресурсах свои типы. Я что-то упустил?
            0
            Первая проблема — мы не можем загрузить второй instance класса Application, при вызове:
            XamlReader.Load(File.OpenRead(Host.ResolvePath("/App.xaml")));
            

            выдается исключение:
            System.InvalidOperationException: Cannot create more than one System.Windows.Application instance in the same AppDomain
            

            Изначально у меня была идея вообще не парсить xaml, а выковырять все из Application.Current, но я не смог найти подходящего решения.
            Плюс такой подход не решает второй проблемы — ресурсы могут быть объявлены не только у корневого элемента, они могут быть и у Grid, который объявлен как дочерний элемент страницы, и у Custom элемента из сторонней библиотеки.
            Мне показалось, что xpath справляется с задачей найти ресурсы на любом уровне вложенности вполне гибко.

            Подход с загрузкой через XamlReader годится для разбора отдельныx файлов c ресурсами, корневым элементом которых является ResourceDictionary.
              –1
              Э… Сгенерированный из скрипта T4 исполняемый файл создаёт WPF Application? Если из WPF приложения пытаться загрузить XAML с Application в корне — это да, упадёт.

              Так или иначе, держать общие для разных компонентов приложения ресурсы в отдельном resource dictionary, а в App.xaml мержить все нужные словари — это разумная практика. Причин держать ресурсы напрямую в App.xaml, в общем-то, нет.

              Ресурсы-то могут лежать во вложенных элементах, но, во-первых, это обычно подразумевает, что ресурсы используются в одном месте, и для расшаривания не предназначены. Хочется дать общий доступ — положите в именованный словарь, будет какая-то организация, а не свалка. Во-вторых, с XPath вы тоже не отловите все случаи, потому что элемент ResourceDictionary может быть неявным, потому что ключи могут использоваться не только для ресурсов, потому что имена могут конфликтовать, потому что нужно понимать типы свойств, потому что достать ресурсы из кода будет закатом солнца вручную и т.п. По сравнению с этим пройтись по понятному построенному из XAML дереву объектов будет гораздо проще (хоть и не просто, но хоть реализуемо).
        0
        Комментарий удален.
          0
          Хочу рассказать, как максимально удобно работать с XAML-ресурсами из Code-Behind. В этой статье мы разберемся, как работают пространства имен XAML, узнаем о XmlnsDefinitionAttribute, используем Т4-шаблоны и сгенерируем статичный класс для доступа к XAML-ресурсам.

          Хотя бы кратко опишите достаточно частый кейс, в котором такое решение необходимо. Мне за все время работы с WPF лишь в единичных случаях приходилось доставать в Code-Behind из контрола ресурс по ключу и подсовывать его в динамически создаваемый новый контрол. С оглядкой на те случаи, сам себе признаю, что был явный Design Flaw.
            0
            В случае с проектом, над которым я сейчас работаю, такое решение удобно, когда в проекте есть несколько глобальных настроек, которые не меняются, например:
            • основной цвет
            • класс с командами навигации
            • класс с командами, которые должны быть доступны на любом экране (например logout)


            В случае с командами это удобно, т.к. я могу либо использовать StaticResource во View, либо вызвать команду из Code-Behind.

            Вы всегда можете передать такие настройки и через DependencyProperty, но зачем?

            Резюмирую: полезно использовать для всего глобального скоупа, который может использоваться в Code-Behind, чтобы не плодить для каждого такого свойства DependencyProperty и использовать Binding, т.к в этом случае это просто лишний код.
              0
              Вы всегда можете передать такие настройки и через DependencyProperty, но зачем?

              Вопрос неверно поставлен. Зачем через код экстрактить ресурсы и перекладывать их куда-либо, если это можно сделать в XAML.
              1. Если у Вас не все ресурсы и/или не во всех случаях возможно получить через ссылки вида StaticResource (или в крайнем случае DynamicResource), то у Вас что-то не так с архитектурой.
              2. Из Вашего резюме ничего не понятно. Зачем в коде получать доступ к цветам? Код должен содержать логику, которую легко протестировать без надобности загрузки ресурсов XAML.
              3. Чтобы расшарить ViewModel с глобальными командами, очень просто инстанциировать MainViewModel как ресурс в App.xaml. Или использовать ObjectDataProvider + Unity Container. В библиотеке MVVMLight есть к тому же реализация паттерна Service Locator, с помощью которой можно легко расшаривать VM с командами.

              Ресурсам в коде делать нечего как минимум с точки зрения тестопригодности.
                0
                все равно непонятно. Для этого же стили существуют

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

            Самое читаемое