XamlResource - Access xaml resources in a strongly typed way





5.00/5 (7 votes)
Access xaml resources in a strongly typed way
XamlResource - Access resources in a strongly typed way

Table of content
Introduction
If you already read my article "XamlVerifier - check or auto correct binding path at compile and design time", you may have guessed that I'm creating a suite of tool to improve Xaml experience.
XamlResourceExtension
is a Genuilder extension that will allow you
to access your static resources in a strongly typed manner.
<Window x:Class="Test.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<DataTemplate x:Key="MyTemplate"></DataTemplate>
</Window.Resources>
<Grid>
</Grid>
</Window>
Save the file and you will be able to access to MyTemplate
in code
behind with the following code :
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataTemplate template = TypedResources.MyTemplate;
}
}
How to use it ?
First, install genuilder as explained here (and vote for it ;)).
Then in your solution add a new Genuilder project. (New Project/Visual C#/Genuilder)

Modify the program.cs file of your Genuilder project to install the XamlResourceExtension
:
static void Main(string[] args)
{
foreach(var project in Projects.InSubDirectories("../..").ExceptForThisAssembly())
{
var ex = new ExtensibilityFeature();
ex.AddExtension(new XamlResourceExtension());
project.InstallFeature(ex);
project.Save();
}
}
Run the Genuilder project, and reload your project.
However, as I do with all my "products", I ship the minimal viable product with limitations to gauge interest, and, if there is interest, I will remove these limitations.
Limitations
Do not browse merged dictionaries
XamlResource
does not support merged dictionaries. It means that if
you create the following Resource dictionary called Dictionary1.xaml
.
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="Toto"></Style>
</ResourceDictionary>
And reference it in MainWindow.xaml.
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Dictionary1.xaml"/>
</ResourceDictionary.MergedDictionaries>
<DataTemplate x:Key="MyTemplate"></DataTemplate>
</ResourceDictionary>
</Window.Resources>
You will not be able to access Toto like this :
var toto = TypedResources.Toto;
I agree, it's not very hard to code... except when you have to deal with packed URI which reference resources in another assembly.
Do not support namespace URI mapping
When you will compile such xaml file :
<Window x:Class="Test.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<DataTemplate x:Key="MyTemplate"></DataTemplate>
</Window.Resources>
<Grid>
</Grid>
</Window>
The XAML namespace of DataTemplate http://schemas.microsoft.com/winfx/2006/xaml/presentation,
but XamlResource
is not smart enough to find the CLR namespace.
On compilation you will have the following warning (that you can disable) :

Everything works fine with DataTemplate
, because its namespace is in
the using section by default in MainWindow.xaml.cs. (using System.Windows;)
If it was not the case, the compilation will fail.
Implementation
Design time compilation
Why intellisense works immediately after I save my xaml file ?
Here is the properties of every xaml files :

Custom Tools tells to MSBuild to compile the current project when you save.
When the project is compiled, Genuilder run and generate code at design time. In
this case MainWindow.Resources.cs
is generated.
//----Copied namespace usings from MainWindow.xaml.cs----------
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
//--------------
namespace Test
{
public partial class MainWindow
{
public class MainWindow_TypedResources
{
public DataTemplate MyTemplate
{
get
{
return (DataTemplate)App.Current.Resources["MyTemplate"];
}
}
}
public MainWindow_TypedResources TypedResources
{
get
{
return new MainWindow_TypedResources();
}
}
}
}
How do I generate this code ? I use XamlResourceReader
to get specific
XAML parts interesting to me and XamlResourceExtension
to generate
the code and pass it to MSBuild.
XamlResourceReader, identify the ResourceHolder and enumerate through XamlResource
Two informations are important for me :
What is the ResourceHolder
, ie, who is the owner of resources ?
For each resources, what is its type and what is its key ?
The XamlResourceReader.ResourceHolder
is specified at the beginning
of the file, in x:Class attribute, in our case : Test.MainWindow.
XamlResourceReader.ResourceHolder
is specified by TypeName
to separate the namespace part and name part.

XamlResourceReader
use XmlReader
to parse XAML files,
fetching the ResourceHolder
is not very hard.
private TypeName _ResourceHolder;
public TypeName ResourceHolder
{
get
{
EnsureResourceHolder();
return _ResourceHolder;
}
}
private void EnsureResourceHolder()
{
if(!_ResourceHolderInitialized)
{
_ResourceHolderInitialized = true;
MoveWhile(xmlReader, IsNotElement);
_ResourceElementName = xmlReader.LocalName + ".Resources";
xmlReader.MoveToAttribute("Class", "http://schemas.microsoft.com/winfx/2006/xaml");
if(String.IsNullOrEmpty(xmlReader.Value))
return;
_ResourceHolder = TypeName.Parse(xmlReader.Value);
while(MoveWhile(xmlReader, IsNotElement))
{
if(xmlReader.Depth == 1 && _ResourceElementName == xmlReader.LocalName)
{
break;
}
if(xmlReader.Depth == 1 && xmlReader.LocalName != _ResourceElementName)
xmlReader.Skip();
}
}
}
I move on the root element (in our case Window), fetch the full class name (Test.MainWindow)
and split the namespace and type part with TypeName.Parse
.
Then, I move on the resource element (Window.Resources).
Now I need to get every XamlResource
. Every XamlResource
have a key (MyTemplate) and a XamlType, which is a type name (DataTemplate) and
a XAML namespace (http://schemas.microsoft.com/winfx/2006/xaml/presentation).
A xaml namespace can a URI or a CLR Namespace in the form of clr-namespace:*(;assembly=*)?.
These concepts are expressed through the following model :

The implementation of XamlResourceReader
is not very difficult, you
can call XamlResourceReader.Read()
to get XamlResource
one after the other.
With the helper method called XamlResourceReader.ReadAll()
I will be
able to use foreach
instead of a while
to iterate through
all XamlResource
public XamlResource Read()
{
EnsureResourceHolder();
if(_ResourceHolder == null)
return null;
do
{
if(xmlReader.EOF || IsEndResource)
return null;
xmlReader.Read();
}
while(IsNotElement(xmlReader));
var xamlType = new XamlType()
{
Name = xmlReader.LocalName,
Namespace = XamlNamespace.Parse(xmlReader.NamespaceURI)
};
if(!xmlReader.MoveToAttribute("Key", "http://schemas.microsoft.com/winfx/2006/xaml"))
return Read();
var result = new XamlResource()
{
Key = xmlReader.Value,
Type = xamlType
};
xmlReader.Skip();
return result;
}
private bool IsEndResource
{
get
{
return xmlReader.NodeType == XmlNodeType.EndElement && xmlReader.LocalName == _ResourceElementName && xmlReader.Depth == 1;
}
}
XamlResourceExtension, plugging everything with MSBuild
Now that I can iterate on all resources in a XAML file, I must generate
the code in MainWindow.Resources.cs with XamlResourceExtension
.

public void Execute(ExtensionContext extensionContext)
{
var xamlFilesQuery = XamlFiles ?? new FileQuery().SelectInThisDirectory(true).All().ToQuery();
var xamlItems = extensionContext.GenItems
.GetByQuery(xamlFilesQuery)
.Where(x => x.SourceType == SourceType.Page || x.SourceType == SourceType.ApplicationDefinition);
foreach(var xamlItem in xamlItems)
{
GenerateTypedResources(xamlItem, extensionContext);
}
}
If XamlResourceExtension.XamlFiles
is not set, I take all files recursively
from the project's directory.
Then I filter pages and the App.xaml file (SourceType.ApplicationDefinition).
For each xaml page, I generate typed resources.
private void GenerateTypedResources(GenItem xamlItem, ExtensionContext extensionContext)
{
FileSet fileSet = new FileSet(xamlItem.Name);
var codeBehindItem = extensionContext.GenItems.GetByNames(fileSet.CodeBehind).FirstOrDefault();
if(!xamlItem.Modified && (codeBehindItem == null || !codeBehindItem.Modified))
return;
using(var fsApp = xamlItem.Open())
{
var reader = new XamlResourceReader(XmlReader.Create(fsApp));
if(reader.ResourceHolder == null)
return;
var resources = reader.ReadAll().ToList();
if(resources.Count == 0)
return;
FileSet is a stucture that will gather the name of the XAML, the name of the codebehind file and the name of the generated file from the xaml file.
I take care to skip the process if the xaml file and the code behind file have not changed since the last time.
And, if there is no resource, I skip as well.
using(var fs = xamlItem.Children.CreateNew(fileSet.Generated).Open())
{
var writer = new CodeWriter(fs);
if(codeBehindItem != null)
{
writer.WriteComment("----Copied namespace usings from " + fileSet.CodeBehind + "----------");
foreach(Match match in Regex.Matches(codeBehindItem.ReadAllText(), "using ([^;]*)"))
{
writer.WriteUsing(match.Groups[1].Value);
}
writer.WriteComment("--------------");
}
IDisposable ns = String.IsNullOrEmpty(reader.ResourceHolder.Namespace) ? null : writer.WriteNamespace(reader.ResourceHolder.Namespace);
writer.Write("public partial class " + reader.ResourceHolder.Name);
writer.NewLine();
using(writer.WriteBrackets())
{
var resourceTypeName = reader.ResourceHolder.Name + "_TypedResources";
writer.Write("public class " + resourceTypeName);
writer.NewLine();
using(writer.WriteBrackets())
{
foreach(var resource in resources)
{
var resourceNs = GetCLRNamespace(resource.Type.Namespace, xamlItem.Logger, fileSet);
var fullName = GetFullName(resourceNs, resource.Type.Name);
writer.Write("public " + fullName + " " + resource.Key);
writer.NewLine();
using(writer.WriteBrackets())
{
writer.Write("get");
writer.NewLine();
using(writer.WriteBrackets())
{
writer.Write("return (" + fullName + ")App.Current.Resources[\"" + resource.Key + "\"];");
}
}
}
}
writer.Write("public " + resourceTypeName + " TypedResources");
writer.NewLine();
using(writer.WriteBrackets())
{
writer.Write("get");
writer.NewLine();
using(writer.WriteBrackets())
{
writer.Write("return new " + resourceTypeName + "();");
}
}
}
if(ns != null)
{
ns.Dispose();
}
writer.Flush();
}
}
}
This code is self explanatory, I just generate the code.
Conclusion
Altough I'm happy with the code, I could have made things better by using a template engine like StringTemplate
or Razor engine
, to generate the code instead of using CodeWriter
.
So it would be possible to generate code easily in multiple languages. But maybe it will be the subject of a futur post.
The goal of this article is both to make Xaml development easier and to show an example of what Genuilder.Extensiblity can do with relatively few line of code.