How to get convenient access to XAML resources from Code-Behind



I want to tell you how it is most convenient to work with XAML resources from Code-Behind. In this article, we will understand how XAML namespaces work, learn about XmlnsDefinitionAttribute , use T4 templates, and generate a static class to access XAML resources.

Introduction


When working with XAML, ResourceDictionary is widely used to organize resources: styles, brushes, and converters. Consider the resource declared in App.xaml:


In the layout of the View, this resource will be used in this way:


When you need to use the same resource from Code-Behind, the construction is usually used:

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

There are a number of drawbacks in it: the string identifier (key) of the resource increases the likelihood of an error, and with a large number of resources, you will most likely have to go into xaml and remember this same key. Another unpleasant trifle is casting to SolidColorBrush because all resources are stored as object.

These shortcomings can be eliminated with the help of code generation, in the end we get the following design:

header.Foreground = AppResources.HeaderBrush;

I must say right away that since the purpose of the article is to show the approach itself, for simplification I focus on one App.xaml file, but if desired, simple modifications will allow you to process all XAML resources in the project and even decompose them into separate files.

Create a T4 template:



If you are not very familiar with T4, you can read this article .

We use the standard for the T4 header:

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

Setting hostSpecific = true is necessary in order to have access to the Host property of the TextTransformation class , from which the T4 template class is inherited. With the help of Host , access will be made to the file structure of the project and to some other necessary data.

All resources will be collected in one static class with static readonly Property. The main skeleton of the template looks like this:

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

All auxiliary functions and properties involved in the script are declared in the <# + #> section after the main body of the script.

The first property of VsProject selects the project from Solution, in which the script itself lies:

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 - project namespace:

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

All the main work of collecting resources from XAML is done by ResourcesFromFile (string filename) . To understand how it works, let us examine in more detail how namespaces, prefixes, and how they are used in XAML are arranged.

Namespaces and Prefixes in XAML


To uniquely point to a specific type in C #, you must completely specify the type name along with the namespace in which it is declared:

var control = new CustomNamespace.CustomControl();

When using using, the above construct can be written shorter:

using CustomNamespace;
var control = new CustomControl();

Namespaces in XAML work similarly. XAML is a subset of XML and uses the rules for declaring namespaces from XML.

The XAML CustomControl type will be declared like this:


In this case, the XAML parser looks at the local prefix when parsing the document , which describes where to look for this type.

xmlns:local="clr-namespace:CustomNamespace"

The reserved attribute name, xmlns , indicates that this is an XML namespace declaration. The prefix name (in this case, “ local ”) can be anything within the framework of XML markup rules. And also it may be absent altogether, then the namespace declaration takes the form:

xmlns="clr-namespace:CustomNamespace"

This entry sets the default namespace for items declared without prefixes. If, for example, the CustomNamespace namespace is declared by default, then CustomControl can be used without a prefix:


In the above example, the xmlns attribute value contains the clr-namespace label , immediately followed by a reference to the .net namespace. Thanks to this, the XAML analyzer understands that it needs to search for CustomControl in the CustomNamespace namespace .

Types included in the SDK, for example, SolidColorBrush are declared without a prefix.


This is possible because the default namespace is declared in the root element of the XAML document:

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

This is the second way to declare a namespace in XAML. The value of the xmlns attribute is some unique alias string, it does not contain clr-namespace . When the XAML parser encounters such an entry, it checks the .net of the project assembly for the XmlnsDefinitionAttribute attribute .

The XmlnsDefinitionAttribute attribute is changed to the assembly many times describing the namespaces corresponding to the alias string:

[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-namespaceand one to many when using alias.

The xmlns construct is typically found in the root element of a XAML document, but in fact it is enough for xmlns to be declared at least at the same level that it is used. In the case of CustomControl, the following entry is possible:


All of the above will be needed to create a script that can correctly understand the ReamurceDictionary XAML markup , which may contain heterogeneous objects included in the SDK, as well as components of third-party libraries that use different methods for declaring namespaces.

Let's get down to the main part


The task of determining the full type name by the XAML tag is assigned to the ITypeResolver interface :

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

Since there are two types of namespace declarations, we have two implementations of this interface:

public class ExplicitNamespaceResolver : ITypeResolver
{
    private string _singleNamespace;
    public ExplicitNamespaceResolver(string singleNamespace)
    {
        _singleNamespace = singleNamespace;
    }
    public string ResolveTypeFullName(string localTagName)
    {
        return _singleNamespace + "." + localTagName;
    }
}

This implementation handles the case where the .net namespace is explicitly specified using clr-namespace.

Another case is answered by 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 registers the namespaces labeled with the XmlnsDefinitionAttribute attribute with a specific alias, and the assemblies in which they are declared. A search is performed in each registered namespace until a result is found.

In the implementation of ResolveTypeFullName you can optionally add caching of the found types.

The TypeResolvers helper method parses the XAML document, finds all the namespaces and maps them to the XML prefix, the output is a 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;
}

Using xpath - "// namespace :: *" selects all namespaces declared at any level in the document. Next, each namespace is parsed by the regular expression into the prefix and the .net namespace specified after clr-namespace , if any. In accordance with the results, either ExplicitNamespaceResolver or XmlnsAliasResolver is created and mapped to the default prefix or prefix.

The ResourcesFromFile method collects everything together:

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


After loading the XAML document and initializing typeResolvers for the xpath to work correctly , the namespace schemas.microsoft.com/winfx/2006/xaml is added to the XmlNamespaceManager , which all key attributes in the ResourceDictionary point to .

When using xpath - "// * [@ x: Key]" , objects with an attribute key are selected from all levels of the XAML document. Next, the script runs through all the found objects and, using the “dictionary” of typeResolvers , associates each with the full name of the .net type.

The output is an array of Resource structures containing all the necessary data for code generation:

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

And finally, a method that displays the resulting Resource as text:

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

It is worth noting that the Key property returns the value of the key attribute from XAML as is, and the use of keys with characters that are not valid for declaring properties in C # will lead to an error. In order not to complicate the already large pieces of code, I intentionally leave the implementation of obtaining property-safe names at your discretion.

Conclusion


This script works in WPF-, Silverlight-, WindowsPhone-projects. As for the WindowsRT family, UniversalApps, in the following articles we will plunge into XamlTypeInfo.g.cs , talk about IXamlMetadataProvider , which replaced XmlnsDefinitionAttribute and make the script work with UniversalApps.

Under the spoiler you can find the full script code, copy to your project, use with pleasure.

Full script code
<#@ 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];
    }
#>


Also popular now: