📜 ⬆️ ⬇️

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



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

Introduction


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

<Application.Resources> <SolidColorBrush x:Key="HeaderBrush" Color="Black" /> <Application.Resources> 

When the layout of the View this resource will be used as follows:
')
 <TextBlock x:Name="header" Foreground="{StaticResource HeaderBrush}" /> 

When it is necessary to use the same resource from Code-Behind, the following is usually applied:

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

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

These shortcomings can be eliminated with the help of code generation, eventually this construction will turn out:

 header.Foreground = AppResources.HeaderBrush; 

Immediately, I’ll make a reservation that since the purpose of the article is to show the approach itself, for simplicity, I focus on a single App.xaml file, but if you like, simple modifications will allow you to process all XAML resources in a 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 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 class of the T4 template is inherited. With the help of Host, the project file structure and some other necessary data will be accessed.

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 VsProject selects a 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 basic work of collecting resources from XAML is done by ResourcesFromFile (string filename) . In order to understand how it works, let's take a closer look at how namespaces, prefixes, and how they are used are arranged in XAML.

Namespaces and Prefixes in XAML


To uniquely identify a specific type in C #, you must fully specify the name of the type along with the namespace in which it is declared:

 var control = new CustomNamespace.CustomControl(); 

Using using the above construction can be written shorter:

 using CustomNamespace; var control = new CustomControl(); 

XAML namespaces work in the same way. XAML is a subset of XML and uses namespace declaration rules from XML.

The CustomControl type in XAML will be declared like this:

 <local:CustomControl /> 

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

 xmlns:local="clr-namespace:CustomNamespace" 

The reserved attribute name, xmlns , indicates that it is an XML namespace declaration. The prefix name (in this case “ local ”) can be anything within the rules of the XML markup. 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 elements declared without prefixes. If, for example, the CustomNamespace namespace is declared by default, then CustomControl can be used without the prefix:

 <CustomControl /> 

In the example above, the value of the xmlns attribute contains the clr-namespace tag, immediately followed by a reference to the .net namespace. Thanks to this, the XAML parser understands that he needs to look for CustomControl in the CustomNamespace namespace.

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

 <SolidColorBrush Color="Red" /> 

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 a clr-namespace . When the XAML parser encounters such an entry, it checks the project's .net assembly for the XmlnsDefinitionAttribute attribute.

The XmlnsDefinitionAttribute attribute changes to an 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")] 

The System.WIndows assembly is tagged with many such attributes, so alias schemas.microsoft.com/winfx/2006/xaml/presentation includes many namespaces from the standard SDK such as System.Windows, System.Windows.Media, etc. . This allows you to map an XML namespace to a set of namespaces from .net

It is worth noting that if there are types with the same name in two namespaces combined under one alias, a collision will occur, and the XAML analyzer will not be able to make out where it will take the desired type.

So now we know that XAML namespaces are mapped to .net namespaces in two different ways: one to one when using clr-namespace and one to many when using alias.

The xmlns construct is usually 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 at which it is used. In the case of CustomControl, the following entry is possible:

 <local:CustomControl xmlns:local="clr-namespace:CustomNamespace" /> 

All of the above will be needed to create a script that can correctly understand the ReosurceDictionary XAML markup, which can contain heterogeneous objects included in the SDK, as well as components of third-party libraries that use different namespace declarations.

Let's start the main part


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

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

Since there are two types of namespace declarations, there are 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 specified explicitly using clr-namespace.

The other case is the 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; } } 

The XmlnsAliasResolver registers within itself the namespaces marked with the XmlnsDefinitionAttribute attribute with a specific alias, and the assemblies in which they are declared. Searches are performed in each registered namespace until a result is found.

Optionally, you can add caching of found types to the implementation of ResolveTypeFullName .

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

Using xpath - "// namespace :: *" selects all namespaces declared at any levels of the document. Then each namespace is parsed by the regular expression into the prefix and the .net namespace, specified after clr-namespace , if it exists. According to the results, either ExplicitNamespaceResolver or XmlnsAliasResolver is created and mapped to a prefix or default prefix.

The ResourcesFromFile method puts 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<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(); } 


After the XAML document is loaded and typeResolvers are initialized, the namespace schemas.microsoft.com/winfx/2006/xaml is added to the xpath to work properly in the XmlNamespaceManager , which all attribute attributes point to in the ResourceDictionary .

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

The output is an array of Resource structures that contain 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 using keys with characters that are not valid for declaring properties in C # will result in an error. In order not to complicate the already large pieces of code, I deliberately leave the implementation of getting safe for Property names at your discretion.

Conclusion


This script works in WPF-, Silverlight-, WindowsPhone-projects. As for the WindowsRT family, UniversalApps, in the next articles we will take a look at XamlTypeInfo.g.cs , talk about IXamlMetadataProvider , which replaced the XmlnsDefinitionAttribute and let the script work with UniversalApps.

Under the spoiler you can find the full script code, copy to your project, use it 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<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]; } #> 

Source: https://habr.com/ru/post/249105/


All Articles