Daniel Vaughan

free range code

Generating Localized Resources in Mono for Android Using T4

clock April 14, 2013 08:17 by author Daniel Vaughan

Introduction

Recently I have been using the Xamarin products: Mono for iOS and Mono for Android, to port some of our Windows Phone apps to iOS and Android. I like the Xamarin products a lot. Being able to contain most of my development activities to the familiar environment of Visual Studio has made getting up to speed easier.

One key facet of the Xamarin products, in particular Mono for Android, is that they do not attempt to overly abstract the APIs of the underlying platform. When you are working with Mono for Android, the class hierarchy, in the most part, mirrors that of the Java Android SDK. This means that you don’t miss out on learning how ‘native’ development is done, and that you aren’t introducing an undue dependency on a third party API.

There isn’t a built-in unified UI framework for Xamarin’s mono products. You still have to define your views separately for each platform. Obviously this adds to development time, yet it makes sense. Each platform has a different look and feel, and users prefer apps that conform to the UI conventions of their particular mobile platform. There are open source projects out there that attempt to bridge the gap. If though, you wish to leverage the UI visual development tools provided by Xcode and Visual Studio, there is no getting away from providing a separate UI implementation for each platform. If you are new to iOS development like I am, then using Xcode’s Interface Builder also allows you to explore and improve your understanding of the UI framework.

Aside from the absence of a unified UI framework, there is also a major difference in the localization model in Mono for Android. Mono for Android uses the same localization model as that of apps built in Java using the Android SDK. This creates an incompatibility with Windows Phone and Mono for iOS projects, which both normally use Resx files for localization.

This article looks at creating a T4 template to generate a Mono for Android localization files from Resx files. You see how to reuse the template with the import directive. You see how to generate a designer class with statically typed properties using T4. Finally the article demonstrates how to dynamically change the UI language at run-time.

Localization Model Differences Between .NET and Mono for Android

Although Mono for Android's localizability model is markedly different to the Resx model, they do have similarities. Android’s string localization model uses XML files, containing key value pairs that declare localized strings in your app. Unlike .NET, however, these files are placed in a Resources directory within your Mono for Android project and compilation to satellite assemblies does not occur.

In Android, you add support for additional languages by creating additional XML files; placing them in folders within the Resources directory that are suffixed with language and region codes. Android chooses the appropriate set of resources depending on the locale that the app is running under. This is much like the .NET hub and spoke localization model, where a default Resx file is complimented by other localized Resx files. If a string cannot be located in a specific language, fall back occurs to the default resource file.

In .NET, Resx files are accompanied by a generated designer class that allows you to reference localized strings using statically typed properties, which gives you compile time assurance that the resource names have been correctly specified in your code. Although Mono for Android also generates a designer class, it surfaces the resources as an integer identifier, which introduces an extra step to retrieving a localized resource. If you link files into your Mono for Android project that make use of the .NET designer class then your project will not compile. The designer class in the .NET project makes use of a ResourceManager object and retrieves localized resources in a manner that does not work in Android. Thus, in addition to needing a way to convert each Resx file to an Android localized string file, you need a way to generate a Mono for Android-friendly designer class.

Converting a Resx File to a Mono for Android Localized XML File

Resx files are XML files comprised of, among other things, data elements, which specify the name of the resource and its localized value (see Listing 1).

Listing 1. Resx File (Excerpt)

<?xml version="1.0" encoding="utf-8"?>
<root>
  ...
  <data name="Greeting" xml:space="preserve">
    <value>Hello</value>
  </data>
</root>

The downloadable sample code includes a T4 template named ResxToAndroidXml.txt (see Listing 2). The file contains a single method named Process that accepts a path to a Resx file and converts the Resx to an Android compatible localized resource file. It does this by loading the Resx file into an XDocument object. Each data node within the Resx is written as a string element in the resulting Android resource file.

Listing 2. ResxToAndroidXml.txt

<#@ output extension=".xml" #>
<#@ template language="C#" hostSpecific="true" #>
<#@ assembly name="System.Core" #>
...
<#@ import namespace="System.Globalization" #><#+

public void Process(string resxRelativePath)
{
    WriteLine("<?xml version=\"1.0\" encoding=\"utf-8\" ?>");
    WriteLine("<resources>");

    XDocument document;
    try
    {
        document = XDocument.Load(resxRelativePath);
    }
    catch (FileNotFoundException)
    {
        WriteLine("<!-- File not found: " + resxRelativePath + " -->");
        WriteLine("</resources>");
        return;
    }

    IEnumerable<XElement> dataElements = document.XPathSelectElements("//root/data");

    foreach (XElement element in dataElements)
    {
        string elementName = element.Attribute("name").Value;
        string elementValue;

        XElement valueElement = element.Element("value");

        if (valueElement != null)
        {
            elementValue = valueElement.Value;
        }
        else
        {
            continue;
        }

        string cleanedValue = elementValue.Replace("'", "\\'");

        WriteLine(string.Format("    <string name=\"{0}\">{1}</string>", elementName, cleanedValue));
    }

    WriteLine("</resources>");    
}
#>

Android requires that localized files be placed in directories named according to the culture and region codes (see Figure 1). The default string resources are placed in a Values directory. Localized resources are placed in directories suffixed with the culture and optionally a region code.

Figure 1. The Sample Android Project Structure

To create a T4 template in your Mono for Android project, add a new text file to the project using the Add New Item dialog, and then rename the file, giving it a .tt file extension.

A String.tt template, which references the ResxToAndroidXml.txt include, is placed in each Values directory. Each specifies a different Resx file in the .NET project, as demonstrated in Listing 3.

Listing 3. Strings.tt

<#@ include file="..\..\..\..\ResxToAndroidXml.txt" #>
<#@ output extension=".xml" #>
<#@ template language="C#" hostSpecific="true" #>
<#
	Process(Path.GetDirectoryName(Host.TemplateFile) 
             + "../../../../AndroidResxT4/Resources/AppResources.resx");
#>

Resulting String.xml files must have their Build Action set to AndroidResource (see Listing 4). You can see that the name attribute of the data element in the Resx file maps to the name attribute of the string element in the Android resource, and that the value element in the Resx file maps to the content of the string element in the Android resource.

Listing 4. Strings.xml

<?xml version="1.0" encoding="utf-8" ?>
<resources>
    <string name="Greeting">Hello</string>
</resources>

Generating Statically Typed Properties for Resource Values

The generated Mono for Android localized string files can be consumed in Mono for Android layout files or in code. Recall, however, that the generated Resx designer class in the .NET project cannot be reused in your Android project. What you need is an independent designer class just for the Mono for Android project that contains the same properties as the .NET Resx designer class. For this we turn to T4 once again.

The ResxToAndroidAccessorClass.txt file in the downloadable sample is a T4 template with a single method named Process. Process accepts the path to your Resx file and the namespace you wish the resulting designer class to reside in (see Listing 5). The method attempts to load the Resx file into an XDocument object and then generates a property element for each data element in the Resx file. The class is placed into the specified namespace. This allows you to align the code with that of your .NET project.

Listing 5. ResxToAndroidAccessorClass.txt Process Method

<#@ output extension=".cs" #>
<#@ template language="C#" hostSpecific="true" #>
<#@ assembly name="System.Core" #>
...
<#@ import namespace="System.Globalization" #><#+

public void Process(string resxRelativePath, string namespaceName)
{
    WriteLine("using Android.App;");
    WriteLine("namespace " + namespaceName);
    WriteLine("{");
    WriteLine("    public class AppResources {");
    //throw new Exception(Directory.GetCurrentDirectory());
    XDocument document;
    try
    {
        document = XDocument.Load(resxRelativePath);
    }
    catch (FileNotFoundException)
    {
        WriteLine("<!-- File not found: " + resxRelativePath + " -->");
        WriteLine("}}");
        return;
    }

    IEnumerable<XElement> dataElements = document.XPathSelectElements("//root/data");

    foreach (XElement element in dataElements)
    {
        string elementName = element.Attribute("name").Value;
        string elementValue;

        XElement valueElement = element.Element("value");

        if (valueElement != null)
        {
            elementValue = valueElement.Value;
        }
        else
        {
            continue;
        }

        string cleanedValue = elementValue.Replace("'", "\\'");

        WriteLine(string.Format("public static string {0} {{ get {{ return Application.Context.GetString(Resource.String.{0}); }}}}", elementName));
    }

    WriteLine("}}");
}
#>

To leverage the ResxToAndroidAccessorClass.txt file, create a T4 file named AppResources.tt in your Mono for Android project. Use the include directive to import the template and call the Process method with the path of the Resx file and the desired namespace (see Listing 6).

Listing 6. AppResources.tt

 <#@ include file="..\..\ResxToAndroidAccessorClass.txt" #>
<#@ output extension=".cs" #>
<#@ template language="C#" hostSpecific="true" #>
<#
	Process(Path.GetDirectoryName(Host.TemplateFile) 
                  + "../../AndroidResxT4/Resources/AppResources.resx", "AndroidResxT4");
#>
 

The template produces a class with properties for all of the localized string in your default resource file (see Listing 7)

Listing 7. AppResources.cs in Mono for Android Project

using Android.App;
namespace AndroidResxT4
{
	public class AppResources {
		public static string Greeting { get { return Application.Context.GetString(Resource.String.Greeting); }}
}}

The AppResources class in your Mono for Android project should mirror your designer class in your .NET project, allowing you to seamlessly link to classes in your .NET project without breaking compilation.

Sample Project Overview

In addition to converting Resx to Android resources and generating a resource designer class, the downloadable sample code demonstrates how to change the UI language of your Mono for Android app at runtime.

The MainActivity class contains a SetLocale method that creates a new Locale object and updated the app’s configuration (see Listing 8). The AttachView and DetachView methods subscribe and unsubscribe to the button’s Click event.

Listing 8. SetLocale Method

void SetLocale(string languageCode)
{
    Resources resources = Resources;
    Configuration configuration = resources.Configuration;
    configuration.Locale = new Java.Util.Locale(languageCode);
    DisplayMetrics displayMetrics = resources.DisplayMetrics;
    resources.UpdateConfiguration(configuration, displayMetrics);

    DetachView();

    SetContentView(Resource.Layout.Main);

    AttachView();
}

Updating the configuration with a French Locale, specified using the ‘fr’ language code, causes the text of a TextView to change from Hello to Bonjour, as shown in Figure 2.

Figure 2. Tapping the Change Locale Button Switches the Language

The AppResources class can be used as it would in a .NET app. Notice that tapping the button writes the localized Greeting message to the console (see Listing 9).

Listing 9. Button.Click Handler

void HandleButtonClick(object sender, EventArgs e)
{
    SetLocale(!localeToggled ? "fr" : "en");
    localeToggled = !localeToggled;

    Console.WriteLine(AppResources.Greeting);
}

Summary

This article looked at creating a T4 template to generate a Mono for Android localization file. You saw how to reuse T4 templates using the import directive. You saw how to generate a designer class with statically typed properties using T4. Finally the article demonstrated how to dynamically change the UI language of an Android app at run-time.

If you apply the principles of SoC and avoid mixing platform specific API calls in your app logic, you can maximize code reuse across platforms. I have almost completed porting the entirety of the Calcium SDK base library to Mono for Android and Mono for iOS. Calcium includes numerous features that make building multifaceted maintainable apps easier, including an IoC and DI system and a user preference API. Anyway, more on that later.

Sample: AndroidResxT4_01.zip (52.71 kb)



Using T4 to Generate Pack URIs for XAML Files

clock November 25, 2009 15:25 by author Daniel Vaughan

Yesterday my fellow WPF Disciple Paul Stovell got me thinking about resolving XAML file paths.

As Paul points out, there doesn't appear to be an easy way to locate the URI for a XAML file. Internally, the generated .g.cs makes use of the path, as shown in the following excerpt:

public void InitializeComponent() 
{ 
  if (_contentLoaded) 
  { 
    return; 
  } 
  _contentLoaded = true; 
  System.Uri resourceLocater = new System.Uri("/PageCollection;component/pages/page1.xaml", System.UriKind.Relative); 
  #line 1 "..\..\..\Pages\Page1.xaml" 
  System.Windows.Application.LoadComponent(this, resourceLocater); 
  #line default 
  #line hidden 
}

But, how can we get our hands on it? What I’ve done is to incorporate the generation of XAML resource pack URIs into the T4 template I did a little while ago.

To demonstrate I have created a dummy UserControl in a subfolder in the sample application.

Image Figure: Dummy UserControl has a pack URI generated

 

The resulting output from the T4 template now enables us to determine the path to the XAML file in a safe way. The following excerpt shows the generated Pack URI:

namespace CSharpDesktopClrDemo.XamlMetadata.Folder1.Folder2.Metadata
{
    /// <summary>Metadata for XAML UserControl1.xaml</summary>
    public static class UserControl1XamlMetadata
    {
            /// <summary>Resource pack URI for XAML file.</summary>
            public const string XamlPackUri 
= @"/DanielVaughan.MetaGen.Demo;component/Folder1/Folder2/UserControl1.xaml"; } }

Now we have this, we can write:

Uri uri = new Uri(CSharpDesktopClrDemo.XamlMetadata.Folder1.Folder2.Metadata
                 .UserControl1XamlMetadata.XamlPackUri, UriKind.Relative);
var control = System.Windows.Application.LoadComponent(uri) 
       as DanielVaughan.MetaGen.Demo.Folder1.Folder2.UserControl1;

No more magic string pack URIs!

Download the template and sample application: MetaGen_01_04.zip (393.35 kb)

 



Compile-Time Validation of Composite Object Data Binding Expressions

clock November 7, 2009 18:06 by author Daniel Vaughan

Introduction

Prompted by a recent comment on the T4 Metadata Generation template article, which I released some weeks ago, I have implemented a new mechanism for concatenating property paths. This allows compile time validation of properties that exist on composite or nested members.

Background

Previously I have demonstrated how generated metadata can be used to provide compile-time validation of binding expressions. Rather than using string literals in binding expressions, one is able to use the x:Static markup extension and a T4 generated constant to indicate the binding path; as shown in the following excerpt.

<Label Content="{Binding Path={x:Static Metadata:PersonMetadata.NamePath}}"/>

Overcoming Limitations

This approach works fine when targeting a property from a single instance in a DataContext, but what happens when we wish to target a nested instance’s property? For example, and as demonstrated in the downloadable sample from the article mentioned above, if we have a ListBox populated with Person instances, and we wish to bind a label to the listbox’s SelectedItem.Address.StreetAddress property, we can do so using the following XAML:

<ListBox x:Name="listBox" Background="Black">
  <ListBox.ItemTemplate>
    <DataTemplate>
      <StackPanel Orientation="Horizontal">
        <Label Content="{Binding Path={x:Static Metadata:PersonMetadata.NamePath}}"/>
      </StackPanel>
    </DataTemplate>
  </ListBox.ItemTemplate>
</ListBox>
<Label Content="{Binding ElementName=listBox, 
    Path={Demo:JoinPath 
                SelectedItem, 
                {x:Static Metadata:PersonMetadata.Address}, 
                {x:Static Metadata:AddressMetadata.StreetLine}}}"/>

Here we see a custom MarkupExtension called JoinPathExtension is used to enable the concatenation of path strings to create a PropertyPath that is used to target the nested Address instance. In this case, the string values of ‘SelectedItem’, ‘Address’, and ‘StreetLine’ combine to produce a PropertyPath ‘SelectedItem.Address.StreetLine’.

You will notice, when you open the CS Window1.xaml file in the sample download, that errors are reported for the Path expressions. These don’t prevent the designer from loading in either Visual Studio or Blend. They are, however, annoying.

 

Diagram: Visual Studio Xaml designer errors.

Attempting to resolve this issue I switched to using named arguments. No luck there either I’m afraid, with the x:Static expression resulting in a compile time error:

(Unknown property 'Converter' for type 'MS.Internal.Markup.MarkupExtensionParser+UnknownMarkupExtension' encountered while parsing a Markup Extension. Line x position Y)

My fellow disciple Philipp Sumi has a great post outlining the VS designer bug. 

I have experimented with a number of approaches, including (as Philipp suggests) explicit property syntax, and have settled on the one shown above.

The main parts of the JoinPathExtension are shown:

CS:

/// <summary>
/// Allows a set of property path strings to be concatenates 
/// into a <see cref="PropertyPath"/> instance.
/// </summary>
[MarkupExtensionReturnType(typeof(PropertyPath))]
public class JoinPathExtension : MarkupExtension
{
  readonly List<string> members = new List<string>(); 
 
  public JoinPathExtension()
  {
    /* Intentionally left blank. */
  }
 
  public JoinPathExtension(string member0)
  {
    if (member0 == null)
    {
      throw new ArgumentNullException("member0");
    }
    members.Add(member0);
  }
 
  public JoinPathExtension(string member0, string member1)
    : this(member0)
  {
    if (member1 == null)
    {
      throw new ArgumentNullException("member1");
    }
    members.Add(member1);
  }
 
  public override object ProvideValue(IServiceProvider serviceProvider)
  {
    var path = string.Join(".", members.ToArray());
    var result = new PropertyPath(path);
    return result;
  }
 
  void SetMember(int index, string value)
  {
    if (value == null)
    {
      throw new ArgumentNullException("value");
    }
    if (members.Count < index + 1)
    {
      members.Add(value);
      return;
    }
    members[index] = value;
  }
 
  #region Named member properties
  [ConstructorArgument("member0")]
  public string Member0
  {
    get
    {
      return members[0];
    }
    set
    {
      SetMember(0, value);
    }
  }
 
  public string Member1
  {
    get
    {
      return members[1];
    }
    set
    {
      SetMember(1, value);
    }
  }
 
}

VB.NET:

Imports System.Windows.Markup

Public Class JoinPathExtension
    Inherits MarkupExtension
    ' Methods
    Public Sub New()
        Me.members = New List(Of String)
    End Sub

    Public Sub New(ByVal memberList As String())
        Me.members = New List(Of String)
        If (memberList Is Nothing) Then
            Throw New ArgumentNullException("memberList")
        End If
        Me.members.AddRange(memberList)
    End Sub

    Public Sub New(ByVal member1 As String)
        Me.members = New List(Of String)
        If (member1 Is Nothing) Then
            Throw New ArgumentNullException("member1")
        End If
        Me.members.Add(member1)
    End Sub

    Public Sub New(ByVal member1 As String, ByVal member2 As String)
        Me.New(member1)
        If (member2 Is Nothing) Then
            Throw New ArgumentNullException("member2")
        End If
        Me.members.Add(member2)
    End Sub

    Public Overrides Function ProvideValue(ByVal serviceProvider As IServiceProvider) As Object
        Return New PropertyPath(String.Join(".", Me.members.ToArray), New Object(0 - 1) {})
    End Function


    ' Properties
    <ConstructorArgument("member1")> _
    Public Property Member() As String
        Get
            Return Me.members.Item(0)
        End Get
        Set(ByVal value As String)
            If (value Is Nothing) Then
                Throw New ArgumentNullException("value")
            End If
            If (Me.members.Count < 1) Then
                Me.members.Add(value)
            Else
                Me.members.Item(0) = value
            End If
        End Set
    End Property

    <ConstructorArgument("member2")> _
    Public Property Member2() As String
        Get
            Return Me.members.Item(1)
        End Get
        Set(ByVal value As String)
            If (value Is Nothing) Then
                Throw New ArgumentNullException("value")
            End If
            If (Me.members.Count < 2) Then
                Me.members.Add(value)
            Else
                Me.members.Item(1) = value
            End If
        End Set
    End Property


    ' Fields
    Private ReadOnly members As List(Of String)
End Class

Conclusion

We have seen how by using a custom MarkupExtension we are able to concatenate generated property name constants to produce PropertyPaths, which can be consumed by Path binding expressions. Having the capability to join path expression adds a lot to the flexibility of the generated metadata approach. We are now able to fully express property paths for nested objects in binding expressions, without resorting to string literals; increasing dramatically the flexibility of this approach.

Download the sample code from here.

 

 



CodeProject's Best VB.NET Article for September 2009.

clock October 26, 2009 20:37 by author Daniel Vaughan

I'm pleased to report that the article Project Metadata Generation using T4 has won the Best VB.NET Article award for September 2009.

The article covers both C# and VB.NET implementations.

I am in the process of writing up a new feature of the T4 template that enables concatenation of PropertyPaths, so we can use our static properties in nested expressions, such as binding to a ListBox using the SelectedItem property. Stay tuned.



Banishing String Literals from XAML Resource References

clock October 3, 2009 19:15 by author Daniel Vaughan

Introduction

Since my initial experimentation with generating project metadata data using T4 (Text Template Transformation Toolkit), there have been several obvious opportunities to expand its scope. One such opportunity has been to use T4 to generate static properties representing XAML keys. This serves to reduce the reliance on string literals when referencing resources. I have subsequently augmented my MetadataGeneration.tt template to do just that.

x:Key Property Generation

To demonstrate, I have updated the sample application provided with my previous article, and employed a couple ResourceDictionaries to show how we can reference a ‘default’ dictionary using constant names, and also how we can cross reference with an auxiliary ResourceDictionary, overriding the resources using constant name values.

In the following excerpt we see a button that has its Background defined using a Resource whose key is defined as a static property in a generated class.

<Button 
Background="{StaticResource {x:Static Keys:MainDictionaryXamlMetadata.ButtonBackgroundKey}}"
Margin
="0,5,0,0" Content="Change" HorizontalAlignment="Left" Click="Button_ChangeClick"/>

This is useful, because it means if we modify the name of the background brush in the ResourceDictionary and forget to update references to it, we will be alerted at compile time, rather than at runtime.

The MetadataGeneration.tt template scours your project looking for XAML files, and then generates classes for them containing all x:Key attributes, represented as static properties. As we can see in the following excerpt, that the ButtonBackGround key is defined as a LinearGradientBrush in the MainDictionary.xaml.

MainDictionary.xaml

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <LinearGradientBrush x:Key="ButtonBackground">
        <GradientStop Color="AliceBlue" Offset="0" />
        <GradientStop Color="Yellow" Offset=".7" />
    </LinearGradientBrush>
    <SolidColorBrush x:Key="WindowForegroundBrush" Color="White"/>
</ResourceDictionary>

Being able to reference one ResourceDictionary from another is useful. If we take another ResourceDictionary, which redefines the resources of the first, we are able to do so in a safer way; expressing our intent with a dedicated property, and using the non-literal string key names derived from the MainDictionary.xaml.

SecondaryDictionary.xaml

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Metadata="clr-namespace:CSharpDesktopClrDemo.XamlMetadata.Folder1.Metadata">
    <LinearGradientBrush x:Key="{x:Static Metadata:MainDictionaryXamlMetadata.ButtonBackgroundKey}">
        <GradientStop Color="AliceBlue" Offset="0" />
        <GradientStop Color="Blue" Offset=".7" />
    </LinearGradientBrush>
    <SolidColorBrush x:Key="{x:Static Metadata:MainDictionaryXamlMetadata.WindowForegroundBrushKey}" Color="Azure"/>
</ResourceDictionary>

So, we can define our resources wherever we like; in a separate assembly for example, yet we still retain compile time validation of resource key references.

App.xaml

<Application x:Class="DanielVaughan.MetaGen.Demo.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    StartupUri="Window1.xaml">
    <Application.Resources>
        <ResourceDictionary Source="pack://application:,,,/DanielVaughan.MetaGen.Demo;component/Folder1/MainDictionary.xaml"/>
        <!--<ResourceDictionary Source="pack://application:,,,/DanielVaughan.MetaGen.Demo;component/Folder1/SecondaryDictionary.xaml"/>-->
    </Application.Resources>
</Application>

Implementation

To accomplish the discovery of XAML files and associated Keys, and the subsequent generation of metadata classes, during project traversal we must do two things: detect when the project item is a XAML file, and keep a track of the current project directory. Now accomplishing the first is easy. Detecting when the current project item is a project folder, on the other hand, turned out to be hack-worthy, as you will notice in the following excerpt.

string processingDirectory = string.Empty;

public void ProcessProjectItem(ProjectItem projectItem,
    Dictionary<string, NamespaceBuilder> namespaceBuilders, string activeNamespace)
{
    FileCodeModel fileCodeModel = projectItem.FileCodeModel;

    if (fileCodeModel != null)
    {
        foreach (CodeElement codeElement in fileCodeModel.CodeElements)
        {
            WalkElements(codeElement, null, null, namespaceBuilders);
        }
    }
    
    string activeNamespaceCopy = activeNamespace;
    if (string.IsNullOrEmpty(activeNamespaceCopy))
    {
        if (string.IsNullOrEmpty(xamlRootNamespace))
        {
            activeNamespaceCopy = rootNamespace; 
        }
        else
        {
            activeNamespaceCopy = string.Format("{0}.{1}", 
                rootNamespace, xamlRootNamespace);
        }
    }
    
    if (projectItem.ProjectItems != null 
        && projectItem.ProjectItems.Count > 0)
    {
        /* This is a hack to determine if we have a directory.
            If you know the proper way for doing this, please let me know. */
        try
        {
            var foo = projectItem.Document;
        }
        catch (Exception ex)
        {
            string newNamespace = projectItem.Name.Replace(" ", string.Empty); 
            activeNamespaceCopy += "." + newNamespace; 
        }
    }
    
    string itemName = projectItem.Name; 
    if (generateXamlKeys && itemName.EndsWith(".xaml", true, CultureInfo.InvariantCulture))
    {    
        /* Retrieve or create the namespace builder. */
        NamespaceBuilder namespaceBuilder;

        if (!namespaceBuilders.TryGetValue(activeNamespaceCopy, out namespaceBuilder))
        {
            namespaceBuilder = new NamespaceBuilder(activeNamespaceCopy, null, 0);
            namespaceBuilders[activeNamespaceCopy] = namespaceBuilder;
        }
        
        string fileName = projectItem.get_FileNames(0);
        string text = System.IO.File.ReadAllText(fileName);
        MatchCollection matches = xClassRegex.Matches(text);                

        if (matches.Count > 0)
        {
            string xamlMetadataClassName = ConvertProjectItemNameToTypeOrMemberName(itemName.Substring(0, itemName.Length - 4));                
            var classComments = new List<string> {string.Format("/// <summary>Metadata for XAML {0}</summary>", itemName)};
            XamlBuilder xamlBuiler = new XamlBuilder(xamlMetadataClassName, classComments, 1);
            namespaceBuilder.AddChild(xamlBuiler);
            
            foreach (Match match in matches)
            {
                Group keyGroup = match.Groups["KeyName"];
                string keyName = keyGroup.Value;
                var keyComments = new List<string> {string.Format("/// <summary>Represents x:Key=\"{0}\"/></summary>", keyName)};
                xamlBuiler.AddChild(new XamlKeyBuilder(keyName, keyComments));
            }
        }
    }

    if (projectItem.ProjectItems != null)
    {
        foreach (ProjectItem childItem in projectItem.ProjectItems)
        {
            ProcessProjectItem(childItem, namespaceBuilders, activeNamespaceCopy);
        }
    }
}

We see that generating XAML metadata works in the same way as the class and interface metadata generation, in that we represent the XAML file using a XamlBuilder, and keys within the XAML file are represented as XamlKeyBuilders.

Generating Namespaces for XAML Metadata Classes

To avoid collisions with type names and generated namespace, I offer a customizable xamlRootNamespace configuration variable. This variable is used to construct namespace names for generated XAML metadata classes as the following example illustrates:

If we have a XAML file called Window1.xaml. It will be represented by a class named [generatedClassPrefix]Window1[generatedXamlClassSuffix][generatedClassSuffix]

Conclusion

We have seen how XAML Resource keys, ordinarily referenced using magic strings, can be eliminated using generated Type and File metadata.

I am still rather pleased at what one is able to achieve by combining T4 and the DTE. Visual Studio 2010 will see T4 move to a more visible position within the IDE. This, together with the new features of T4 in VS2010, will surely make it an indispensible tool.

To download the template source and demo applications, please visit the updated T4 Metadata article on Codeproject.

 



Article Published: Project Metadata Generation using T4

clock September 3, 2009 22:58 by author Daniel Vaughan

Metadata Generation with T4 Overview

This article is an elaboration of my previous experimentation with T4 (Text Template Transformation Toolkit) and describes how to use T4, which is built into Visual Studio 2008, and the Visual Studio automation object model API, to generate member and type information for an entire project. Generated metadata can then be applied to such things as dispensing with string literals in XAML binding expressions and overcoming the INotifyPropertyChanged property name string code smell, or indeed any place you need to refer to a property, method, or field by its string name. There is also experimental support for obfuscation, so member names can be retrieved correctly even after obfuscation. I've also ported the template to VB.NET, so our VB friends can join in on the action too.

View Article



MetaGen: A project metadata generator for Visual Studio using T4

clock August 15, 2009 16:31 by author Daniel Vaughan

I am rather excited to share with you something that I have been working on in my spare time for the last couple of days. I have used T4 to build a metadata generator for your Silverlight and Desktop CLR projects. It can be used as a replacement for static reflection (expression trees), reflection (walking the stack), and various other means for deriving the name of a property, method, or field.

There has been much discussion’s around removing the property name string code smell from INotifyPropertyChanged implementations. Reflection is slow, and various techniques using reflection have been proposed, but have been criticized for contributing to decreased application performance. It now seems reasonable that a language extension for property change notification might be in order. But, as we don’t have that yet, I have created the next best thing: a generator.

A couple of days ago I began exploring T4 (Text Template Transformation Toolkit), and I’m loving it. T4, if you don’t already know is versatile templating system that allows you to generate classes, sql scripts etc. from within Visual Studio. If you have Visual Studio 2008, then you already have T4 ready to go! To find out more about T4 visit http://msdn.microsoft.com/en-us/library/bb126445.aspx

How to use it

To use MetaGen, simply include the attached MetaGen.tt file in your project. That’s it!

The MetaGen.tt has a number of customizable constants to prevent name collisions in your project.

/// <summary>
/// The modifier to use when outputting classes.
/// </summary>
const string generatedClassAccessModifier = "internal";

/// <summary>
/// The prefix to use for output class and interface names.
/// The combination of this and <see cref="generatedClassSuffix"/> provides
/// MetaGen with the ability to identify those classes etc.,
/// for which it should generated metadata, and to ignore MetaGen generated classes.
/// </summary>
const string generatedClassPrefix = "";

/// <summary>
/// The suffix to use for output class and interface names.
/// The combination of this and <see cref="generatedClassSuffix"/> provides
/// MetaGen with the ability to identify those classes etc.,
/// for which it should generated metadata, and to ignore MetaGen generated classes.
/// </summary>
const string generatedClassSuffix = "Metadata";

/// <summary>
/// The child namespace in which to place generated items.
/// If there is a class in MyNamespace namespace,
/// the metadata class will be generated
/// in the MyNamespace.[generatedClassSuffix] namespace.
/// This string can be null or empty, in which case a subnamesapce
/// will not be created, and generated output will reside
/// in the original classes namespace.
/// </summary>
const string generatedNamespace = "Metadata";

/// <summary>
/// The number of spaces to insert for a one step indent.
/// </summary>
const int tabSize = 4;

Template Implementation

The template consists of a procedural portion of code that retrieves the Visual Studio EnvDTE.DTE instance. This allows us to manipulate the Visual Studio automation object model, and to retrieve file, class, and method information and so on. Thanks go out to Oleg Sych for the T4 Toolbox which demonstrated how to retrieve the EnvDTE.DTE from the template hosting environment.

Once we object the relevant EnvDTE.Project we are able to process the EnvDTE.ProjectItems (files and directories in this case) as the following excerpt shows:

    IServiceProvider hostServiceProvider = (IServiceProvider)Host;
EnvDTE.DTE dte = (EnvDTE.DTE)hostServiceProvider.GetService(typeof(EnvDTE.DTE));
EnvDTE.ProjectItem containingProjectItem = dte.Solution.FindProjectItem(Host.TemplateFile);
Project project = containingProjectItem.ContainingProject;

/* Build the namespace representations, which contain class etc. */
Dictionary<string, NamespaceBuilder> namespaceBuilders = new Dictionary<string, NamespaceBuilder>();
foreach (ProjectItem projectItem in project.ProjectItems)
{
ProcessProjectItem(projectItem, namespaceBuilders);
}

We then recursively process EnvDTE.CodeElements and directories in order to create an object model representing the project.

 

public void ProcessProjectItem(ProjectItem projectItem, Dictionary<string, NamespaceBuilder> namespaceBuilders)
{
FileCodeModel fileCodeModel = projectItem.FileCodeModel;

if (fileCodeModel != null)
{
foreach (CodeElement codeElement in fileCodeModel.CodeElements)
{
WalkElements(codeElement, null, null, namespaceBuilders);
}
}

if (projectItem.ProjectItems != null)
{
foreach (ProjectItem childItem in projectItem.ProjectItems)
{
ProcessProjectItem(childItem, namespaceBuilders);
}
}

}

int indent;

public void WalkElements(CodeElement codeElement, CodeElement parent,
BuilderBase parentContainer, Dictionary<string, NamespaceBuilder> namespaceBuilders)
{
indent++;
CodeElements codeElements;

if (parentContainer == null)
{
NamespaceBuilder builder;
string name = "global";
if (!namespaceBuilders.TryGetValue(name, out builder))
{
builder = new NamespaceBuilder(name, null, 0);
namespaceBuilders[name] = builder;
}
parentContainer = builder;
}

switch(codeElement.Kind)
{
/* Handle namespaces */
case vsCMElement.vsCMElementNamespace:
{
CodeNamespace codeNamespace = (CodeNamespace)codeElement;
string name = codeNamespace.FullName;
if (!string.IsNullOrEmpty(generatedNamespace) && name.EndsWith(generatedNamespace))
{
break;
}

NamespaceBuilder builder;

if (!namespaceBuilders.TryGetValue(name, out builder))
{
builder = new NamespaceBuilder(name, null, 0);
namespaceBuilders[name] = builder;
}

codeElements = codeNamespace.Members;
foreach (CodeElement element in codeElements)
{
WalkElements(element, codeElement, builder, namespaceBuilders);
}
break;
}
/* Process classes */
case vsCMElement.vsCMElementClass:
{
CodeClass codeClass = (CodeClass)codeElement;
string name = codeClass.Name;
if (string.IsNullOrEmpty(generatedNamespace)
&& name.StartsWith(generatedClassPrefix)
&& name.EndsWith(generatedClassSuffix))
{
break;
}

List<string> comments = new List<string>();
comments.Add(string.Format("/// <summary>Metadata for class <see cref=\"{0}\"/></summary>", codeClass.FullName));

BuilderBase builder;
if (!parentContainer.Children.TryGetValue(name, out builder))
{
builder = new ClassBuilder(name, comments, indent);
parentContainer.Children[name] = builder;
}
codeElements = codeClass.Members;
if (codeElements != null)
{
foreach (CodeElement ce in codeElements)
{
WalkElements(ce, codeElement, builder, namespaceBuilders);
}
}
break;
}
/* Process interfaces. */
case vsCMElement.vsCMElementInterface:
{
CodeInterface codeInterface = (CodeInterface)codeElement;
string name = codeInterface.Name;
if (name.StartsWith(generatedClassPrefix) && name.EndsWith(generatedClassSuffix))
{
break;
}
List<string> comments = new List<string>();
string commentName = FormatTypeNameForComment(codeInterface.FullName);
comments.Add(string.Format("/// <summary>Metadata for interface <see cref=\"{0}\"/></summary>", commentName));
InterfaceBuilder builder = new InterfaceBuilder(name, comments, indent);
parentContainer.AddChild(builder);

codeElements = codeInterface.Members;
if (codeElements != null)
{
foreach (CodeElement ce in codeElements)
{
WalkElements(ce, codeElement, builder, namespaceBuilders);
}
}
break;
}
/* Process methods */
case vsCMElement.vsCMElementFunction:
{
CodeFunction codeFunction = (CodeFunction)codeElement;
if (codeFunction.Name == parentContainer.Name
|| codeFunction.Name == "ToString"
|| codeFunction.Name == "Equals"
|| codeFunction.Name == "GetHashCode"
|| codeFunction.Name == "GetType"
|| codeFunction.Name == "MemberwiseClone"
|| codeFunction.Name == "ReferenceEquals")
{
break;
}

string name = codeFunction.Name.Replace('.', '_');
List<string> comments = new List<string>();
string commentName = FormatTypeNameForComment(codeFunction.FullName);
comments.Add(string.Format("/// <summary>Name of method <see cref=\"{0}\"/></summary>", commentName));
MemberBuilder builder = new MemberBuilder(name, comments, indent);
parentContainer.AddChild(builder);
break;
}
/* Process properties. */
case vsCMElement.vsCMElementProperty:
{
CodeProperty codeProperty = (CodeProperty)codeElement;

string name = codeProperty.Name.Replace('.', '_');
if (name != "this")
{
List<string> comments = new List<string>();
string commentName = FormatTypeNameForComment(codeProperty.FullName);
comments.Add(string.Format("/// <summary>Name of property <see cref=\"{0}\"/></summary>", commentName));
MemberBuilder builder = new MemberBuilder(name, comments, indent);
parentContainer.AddChild(builder);
}
break;
}
/* Process fields. */
case vsCMElement.vsCMElementVariable:
{
CodeVariable codeVariable = (CodeVariable)codeElement;
string name = codeVariable.Name;
List<string> comments = new List<string>();
string commentName = FormatTypeNameForComment(codeVariable.FullName);
comments.Add(string.Format("/// <summary>Name of field <see cref=\"{0}\"/></summary>", commentName));
MemberBuilder builder = new MemberBuilder(name, comments, indent);
parentContainer.AddChild(builder);
break;
}
}
indent--;
}

Once this is complete we output our namespace representations to the resulting MetaGen.cs file like so:

/* Finally, write them to the output. */
foreach (object item in namespaceBuilders.Values)
{
WriteLine(item.ToString());
}

What results is a file containing various namespace blocks that include static classes representing our non-metadata classes and interfaces with the project. Property names, method names, and field names are represented as constants. Inner classes are represented as nested static classes.

I have included with this post the MetaGen.tt template file, and also a demo WPF application. If you have any suggestions such as ideas for other metadata information etc., please let me know.

MetaGen.tt (14.92 kb)  (Just the T4 template)

MetaGen.zip (60.08 kb)  (The T4 template and an demo application)

Update: I've published a more recent article on CodeProject with a newer enriched template and examples.

Please note: If you are after the latest and greatest version of this template, please procure it from the Calcium SDK (in the core project).



Order the Book

Ready to take your Windows Phone development skills to the next level? My book is the first comprehensive, start-to-finish developer's guide to Microsoft's Windows Phone 8. In it I teach through complete sample apps that illuminate each key concept with fully explained code and real-world context. Windows Phone 8 Unleashed

Windows Phone Experts Windows Phone Experts
LinkedIn Group

 

Bio

Daniel VaughanDaniel Vaughan is co-founder and president of Outcoder, a Swiss software and consulting company dedicated to creating best-of-breed user experiences and leading-edge back-end solutions, using the Microsoft stack of technologiesin particular WPF, WinRT, and Windows Phone. 

Daniel is a four-time Microsoft MVP for Client Application Development, with experience across a wide range of industries including finance, e-commerce, and digital media. 
Daniel is the author of Windows Phone 7.5 Unleashed and Windows Phone 8 Unleashed, published by SAMS.

Daniel is a Silverlight and WPF Insider, a member of the WPF Disciples, and a member of the Microsoft Developer Guidance Advisory Council.
Daniel also sits on the advisory board of PebbleAge, a Swiss Financial Software company.

While originally from Australia and the UK, Daniel is currently based in Zurich Switzerland. 

Daniel is the developer behind several acclaimed Windows Phone apps including Intellicam and Splashbox; and is the creator of a number of popular open-source projects including the Calcium and Clog.

Daniel also manages the Windows Phone Experts group on LinkedIn; a group that has over 3000 independent developers, Microsoft employees, and Windows Phone enthusiasts.


E-mail Send mail

 

Microsoft MVP logo Disciple
WPF and Silverlight Insiders
 

 

 

Sign in