ILearnable .Net

July 13, 2010

Using T4 to generate strongly typed translations in episerver (+ how to fire T4 on builds)

Filed under: Uncategorized — andreakn @ 12:49
Tags: , ,

Problem
Typically when developing with EPiServer in the past I have created a helper method to provide a shorthand for accessing language strings:

public static class Lang
{
  public static string Translate(string key)
  {
    //call standard EPiServer API
  }
  public static string Translate(string key, string defaultValue)
  {
    //call standard EPiServer API, return defaultValue if no match found
  }
}

the trouble is the key parameter, it is on the form “/Forms/RegisterForm/Firstname” for instance, which means that every developer needs to know which language strings have been defined and get their full xpath from the language strings (no spelling errors, please), this makes discovering common strings a bit painful as Visual studio does not provide the handy navigator in the bottom for xml files as it does for .aspx files.

I wanted to point T4 at one (or more) language file(s) and get generated accessors so I could get intellisense when accessing language strings.

Solution
what I ended up with was this T4 code (which I put into a file called EPiLanguageAccessors.tt:

<#@ template debug="true" hostSpecific="true" #>
<#@ assembly name="System.Xml"#>
<#@ assembly name="System.Xml.Linq" #>
<#@ assembly name="System.Core" #> 
<#@ import namespace="System.Xml.Linq"#>
<#@ import namespace="System.Linq"#>
<#@ import namespace="System.Collections.Generic"#>


<#
int skipLevels = 2; //depends on xml file, for standard episerver use 2
string xmlFilePath = @"\lang\lang.xml"; 
string baseCodeGenNamespace = "Lang";
XDocument xdoc = XDocument.Load(System.IO.Path.GetDirectoryName(this.Host.TemplateFile)+xmlFilePath);

GenerateLanguageAccessors(xdoc.Root, new List<string>(),baseCodeGenNamespace,skipLevels, new List<string>());
        
#>

<#+

 void GenerateLanguageAccessors(XNode xNode, List<string> ns, string baseNS, int skipLevels, List<string> cache)
        {
            if (xNode is XText)
            {
                if (ns.Count < skipLevels) return;
                string theNameSpace = string.Join(".", ns.Skip(skipLevels).Take(ns.Count - (skipLevels)).ToArray());
                if (string.IsNullOrEmpty(theNameSpace))
                {
                    theNameSpace = baseNS;
                }
                else
                {
                    theNameSpace = baseNS + "." + theNameSpace;
                }
                string theKey = "/" + string.Join("/", ns.Skip(skipLevels).ToArray());
                string s = "namespace " + theNameSpace + "{ public static partial class Get{ public static string Text(){ return Utils.LanguageUtil.Translate(\"" + theKey + "\");} public static string TextOrDefault(string defaultString){ return Utils.LanguageUtil.Translate(\"" + theKey + "\",defaultString);} } }";
               	if(!cache.Contains(theNameSpace))
                {
                    this.WriteLine(s);
               	 	cache.Add(theNameSpace);
                }
				 return;
            }
            if (xNode is XElement)
            {
                var xElement = xNode as XElement;
                var newNameSpace = ns.ToArray().ToList();
                newNameSpace.Add(xElement.Name.ToString());
                foreach (var subNode in xElement.Nodes())
                {
                    GenerateLanguageAccessors(subNode, newNameSpace, baseNS, skipLevels, cache);
                }
            }
        }
		#>

The top part of the .tt file is needed to reference the right stuff for the text transformation,
the middle part is where you specify the specifics of what you want, for instance skiplevel is needed to skip “/language/language” which is a part of the xml files, but not used for accessing strings, also here is where I define the relative path to the language file which is the “master” which will be used for code generation.
the bottom part is the algorithm which creates namespaces for each string with translation logic.

so where previously I would have to use a magic string in the call to LanguageUtil.Translate("/App/Registration/Step1/EulaText"); I can now call Lang.App.Registration.Step1.EulaText.Get.Text();

Getting it to run on each build

T4 out of the box runs the transformation each time the template file is changed. In our case that is not too useful, as we would rather have the transformation run each time the “master” language file is changed. AFAIK it isn’t possible to instruct T4 to fire when some custom file is changed, but you can get it to fire on each build.

There are a few options, for a run down you might want to head over to http://www.olegsych.com which is a goldmine of T4 information.
I found the simplest option to spread across a team (no need to install anything extra to the dev machines) is to create a file called FireT4OnBuild.targets, put it into the root of the project and populate it like this:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <Target Name="ExecuteT4Templates">
        <ItemGroup>
            <T4Templates Include=".\**\*.tt" />
        </ItemGroup>
         <Exec
            WorkingDirectory="C:\Program Files (x86)\Common Files\microsoft shared\TextTemplating\10.0\"
            Command="TextTransform &quot;%(T4Templates.FullPath)&quot; -out &quot;%(T4Templates.RootDir)%(T4Templates.Directory)%(T4Templates.Filename).cs&quot; " /> 
    </Target>
</Project>

You might want to change the path for the working directory (my setup is for .net 4.0 running on windows 7 (64bit)
if the devs use different setups you might want to copy the texttemplating files to a common path (SolutionDir/Lib/T4 for instance) and reference that path

Then you need to unload the project containing the transformation (I’m guessing your xxxxx.Web.csproj ) and edit the .csproj file (right click => unload, then right click => edit )
and add the line

 <Import Project="$(MSBuildProjectDirectory)\FireT4OnBuild.targets" />

in the bottom of the file (put it after the import of Microsoft.CSharp.targets) and changing the defaulttargets to “ExecuteT4Templates;Build”

Now T4 should be firing every time you touch the .tt file(s) and also every time you perform a build. Since ExecuteT4Templates is put before Build the generated code will be in place in time for the build

July 6, 2010

JSON serializing / deserializing, quick and dirty using C# / DataContractSerializer

Filed under: Uncategorized — andreakn @ 06:34

If you need to serialize / deserialize JSON for communicating with javascript for instance you can use the .net DataContractSerializer. The only requirement is that the classes be tagged with [DataContract] and all the members you want to survive the transformation be tagged with [DataMember] (all the built in data types are already [DataContract]s )


using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization.Json;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
namespace Utilities

 static class JsonUtil
{
public static string Serialize(object obj)
{
var jsonSerializer = new DataContractJsonSerializer(obj.GetType());
string returnValue = "";
using (var memoryStream = new MemoryStream())
{
using (var xmlWriter = JsonReaderWriterFactory.CreateJsonWriter(memoryStream))
{
jsonSerializer.WriteObject(xmlWriter, obj);
xmlWriter.Flush();
returnValue = Encoding.UTF8.GetString(memoryStream.GetBuffer(), 0, (int)memoryStream.Length);
}
}
return returnValue;
}

public static T Deserialize<T>(string json)
{
T returnValue;
using (var memoryStream = new MemoryStream())
{
byte[] jsonBytes = Encoding.UTF8.GetBytes(json);
memoryStream.Write(jsonBytes, 0, jsonBytes.Length);
memoryStream.Seek(0, SeekOrigin.Begin);
using (var jsonReader = JsonReaderWriterFactory.CreateJsonReader(memoryStream, Encoding.UTF8, XmlDictionaryReaderQuotas.Max, null))
{
var serializer = new DataContractJsonSerializer(typeof (T));
returnValue = (T) serializer.ReadObject(jsonReader);

}
}
return returnValue;
}
}
}

Blog at WordPress.com.