Click here to Skip to main content
15,880,967 members
Articles / Programming Languages / C#

XamlResource - Access xaml resources in a strongly typed way

Rate me:
Please Sign up or sign in to vote.
5.00/5 (7 votes)
21 Jan 2012Ms-PL4 min read 31.6K   178   16   9
Access xaml resources in a strongly typed way

XamlResource - Access resources in a strongly typed way

Image 1

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.

XML
<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 :

C#
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)

Image 2

Modify the program.cs file of your Genuilder project to install the XamlResourceExtension:

C#
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.

XML
<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.

XML
<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 :

C#
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 :

XML
<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) :

Image 3

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 :

Image 4

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.

C#
//----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.

Image 5

XamlResourceReader use XmlReader to parse XAML files, fetching the ResourceHolder is not very hard.

C#
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 :

Image 6

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

C#
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.

Image 7
C#
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.

C#
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.

C#
        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.

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)


Written By
Software Developer Freelance
France France
I am currently the CTO of Metaco, we are leveraging the Bitcoin Blockchain for delivering financial services.

I also developed a tool to make IaaS on Azure more easy to use IaaS Management Studio.

If you want to contact me, go this way Smile | :)

Comments and Discussions

 
GeneralMy vote of 5 Pin
Vincent BOUZON20-Jan-12 7:10
Vincent BOUZON20-Jan-12 7:10 
GeneralRe: My vote of 5 Pin
Nicolas Dorier20-Jan-12 7:14
professionalNicolas Dorier20-Jan-12 7:14 
GeneralRe: My vote of 5 Pin
Vincent BOUZON20-Jan-12 7:16
Vincent BOUZON20-Jan-12 7:16 
General:) Pin
Code098720-Jan-12 6:48
Code098720-Jan-12 6:48 
GeneralRe: :) Pin
Nicolas Dorier20-Jan-12 7:09
professionalNicolas Dorier20-Jan-12 7:09 
GeneralMy vote of 5 Pin
Nick Polyak20-Jan-12 5:18
mvaNick Polyak20-Jan-12 5:18 
GeneralRe: My vote of 5 Pin
Nicolas Dorier20-Jan-12 6:02
professionalNicolas Dorier20-Jan-12 6:02 
QuestionAnother nice one Pin
Sacha Barber20-Jan-12 0:11
Sacha Barber20-Jan-12 0:11 
AnswerRe: Another nice one Pin
Nicolas Dorier20-Jan-12 2:01
professionalNicolas Dorier20-Jan-12 2:01 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.