Zur Zeit wird gefiltert nach: templatefilemanager
Filter zurücksetzen

Entity Framework - DbContext und der 2nd-Level Cache mit dem EFCachingProvider

Der DbContext stellt eigentlich ein Wrapper auf den ObjectContext dar. Ich bevorzuge mittlerweile den DbContext, da dieser einige Funktionalitäten bietet, die ich beim ObjectContext vermisse. Da wären bspw. das vereinfachte Change Tracking, der Support der DataAnnotations, der veinfachte Zugriff auf dem Context-Cache über die Local-Eigenschaft und das dieser neben Code First auch mit dem EDM-Designer verwendet werden kann.

Ein Nachteil ist jedoch die fehlende Unterstützung für das Caching. Für den ObjectContext gibt es den EFCachingProvider, der seine Aufgaben relativ gut verrichtet. Über den Konstruktor im ObjectContext wird dieser aktiviert.

Nun ist der DbContext ja nichts anderes als ein Wrapper und diesen kann auch eine ObjectContext-Instanz übergeben werden. Also kam ich auf eine ganz „wilde“ Idee, um den DbContext mit einer Zusammenarbeit mit dem EFCachingProvider zu überreden. Alternativ liesse sich das auch mit dem Proxy-Pattern realisieren, aber ich gebe die Hoffnung nicht auf, dass der EFCachingProvider ein fester Bestandteil vom EF werden wird.

Mit Hilfe der mitgelieferten T4-Vorlage, die den Einsatz des DbContext für DbFirst und ModelFirst ermöglicht, lässt sich der Caching-Support sehr einfach realisieren. Die angepasste T4-Vorlage sieht so aus:


<#@ template language="C#" debug="false" hostspecific="true"#>
<#@ include file="EF.Utility.CS.ttinclude"#><#@
 output extension=".cs"#><#

var loader = new MetadataLoader(this);
var region = new CodeRegion(this);
var inputFile = @"SimpleModel.edmx";
var ItemCollection = loader.CreateEdmItemCollection(inputFile);

Code = new CodeGenerationTools(this);
EFTools = new MetadataTools(this);
ObjectNamespace = Code.VsNamespaceSuggestion();
ModelNamespace = loader.GetModelNamespace(inputFile);

EntityContainer container = ItemCollection.GetItems<EntityContainer>().FirstOrDefault();
if (container == null)
{
    return string.Empty;
}
#>
//------------------------------------------------------------------------------
// <auto-generated>
// <#=GetResourceString("Template_GeneratedCodeCommentLine1")#>
//
// <#=GetResourceString("Template_GeneratedCodeCommentLine2")#>
// <#=GetResourceString("Template_GeneratedCodeCommentLine3")#>
// </auto-generated>
//------------------------------------------------------------------------------

<#

if (!String.IsNullOrEmpty(ObjectNamespace))
{
#>
namespace <#=Code.EscapeNamespace(ObjectNamespace)#>
{
<#
    PushIndent(CodeRegion.GetIndent(1));
}

#>
using System;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.EntityClient;
using System.Data.Objects;
using System.IO;
using EFProviderWrapperToolkit;
using EFTracingProvider;
using EFCachingProvider;
using EFCachingProvider.Caching;

// Workaround, create an objectcontext for 2nd Level Cache with EFProviderToolkit
<#=Accessibility.ForType(container)#> partial class <#=Code.Escape(container)#>Context : ObjectContext
{
	private TextWriter logOutput;
	
    #region Konstruktoren
    public <#=Code.Escape(container)#>Context() 
    : base(EntityConnectionWrapperUtils.CreateEntityConnectionWithWrappers(
                    "name=<#=container.Name#>",
                    /*"EFTracingProvider",*/
                    "EFCachingProvider"
            ), "<#=container.Name#>")

	{
		
    }
	
	public <#=Code.Escape(container)#>Context(string connectionString)
            : base(EntityConnectionWrapperUtils.CreateEntityConnectionWithWrappers(
                    connectionString,
                    /*"EFTracingProvider",*/
                    "EFCachingProvider"
            ))
    {
    }
    #endregion
	
	// ObjectSets are not required, when we use the DbContext.
	
    #region Tracing Extensions

    private EFTracingConnection TracingConnection
    {
        get { return this.UnwrapConnection<EFTracingConnection>(); }
    }

    public event EventHandler<CommandExecutionEventArgs> CommandExecuting
    {
        add { this.TracingConnection.CommandExecuting += value; }
        remove { this.TracingConnection.CommandExecuting -= value; }
    }

    public event EventHandler<CommandExecutionEventArgs> CommandFinished
    {
        add { this.TracingConnection.CommandFinished += value; }
        remove { this.TracingConnection.CommandFinished -= value; }
    }

    public event EventHandler<CommandExecutionEventArgs> CommandFailed
    {
        add { this.TracingConnection.CommandFailed += value; }
        remove { this.TracingConnection.CommandFailed -= value; }
    }

    private void AppendToLog(object sender, CommandExecutionEventArgs e)
    {
        if (this.logOutput != null)
        {
            this.logOutput.WriteLine(e.ToTraceString().TrimEnd());
            this.logOutput.WriteLine();
        }
    }

    public TextWriter Log
    {
        get { return this.logOutput; }
        set
        {
            if ((this.logOutput != null) != (value != null))
            {
                if (value == null)
                {
                    CommandExecuting -= AppendToLog;
                }
                else
                {
                    CommandExecuting += AppendToLog;
                }
            }

            this.logOutput = value;
        }
    }


    #endregion

    #region Caching Extensions

    private EFCachingConnection CachingConnection
    {
        get { return this.UnwrapConnection<EFCachingConnection>(); }
    }

    public ICache Cache
    {
        get { return CachingConnection.Cache; }
        set { CachingConnection.Cache = value; }
    }

    public CachingPolicy CachingPolicy
    {
        get { return CachingConnection.CachingPolicy; }
        set { CachingConnection.CachingPolicy = value; }
    }

    #endregion
}
// Workaround ends here

// DbContext
<#=Accessibility.ForType(container)#> partial class <#=Code.Escape(container)#> : DbContext
{
	// Modification: Constructor with ObjectContext init
    public <#=Code.Escape(container)#>()
        : base(new <#=Code.Escape(container)#>Context(), true)
    {
<#
        WriteLazyLoadingEnabled(container);
#>
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        throw new UnintentionalCodeFirstException();
    }

<#
    foreach (var entitySet in container.BaseEntitySets.OfType<EntitySet>())
    {
#>
    <#=Accessibility.ForReadOnlyProperty(entitySet)#> DbSet<<#=Code.Escape(entitySet.ElementType)#>> <#=Code.Escape(entitySet)#> { get; set; }
<#
    }

    foreach (var edmFunction in container.FunctionImports)
    {
        WriteFunctionImport(edmFunction, false);
    }
#>
}
<#

if (!String.IsNullOrEmpty(ObjectNamespace))
{
    PopIndent();
#>
}
<#
}
#>
<#+
string ModelNamespace { get; set; }
string ObjectNamespace { get; set; }
CodeGenerationTools Code { get; set; }
MetadataTools EFTools { get; set; }

string GetResourceString(string resourceName)
{
	if(_resourceManager == null)
	{
		_resourceManager = new System.Resources.ResourceManager("System.Data.Entity.Design", typeof(System.Data.Entity.Design.MetadataItemCollectionFactory).Assembly);
	}
	
    return _resourceManager.GetString(resourceName, null);
}
System.Resources.ResourceManager _resourceManager;

void WriteLazyLoadingEnabled(EntityContainer container)
{
   string lazyLoadingAttributeValue = null;
   var lazyLoadingAttributeName = MetadataConstants.EDM_ANNOTATION_09_02 + ":LazyLoadingEnabled";
   if(MetadataTools.TryGetStringMetadataPropertySetting(container, lazyLoadingAttributeName, out lazyLoadingAttributeValue))
   {
       bool isLazyLoading;
       if(bool.TryParse(lazyLoadingAttributeValue, out isLazyLoading) && !isLazyLoading)
       {
#>
        this.Configuration.LazyLoadingEnabled = false;
<#+
       }
   }
}

void WriteFunctionImport(EdmFunction edmFunction, bool includeMergeOption)
{
    var parameters = FunctionImportParameter.Create(edmFunction.Parameters, Code, EFTools);
    var paramList = String.Join(", ", parameters.Select(p => p.FunctionParameterType + " " + p.FunctionParameterName).ToArray());
    var returnType = edmFunction.ReturnParameter == null ? null : EFTools.GetElementType(edmFunction.ReturnParameter.TypeUsage);
    var processedReturn = returnType == null ? "int" : "ObjectResult<" + MultiSchemaEscape(returnType) + ">";

    if (includeMergeOption)
    {
        paramList = Code.StringAfter(paramList, ", ") + "MergeOption mergeOption";
    }
#>

    <#=AccessibilityAndVirtual(Accessibility.ForMethod(edmFunction))#> <#=processedReturn#> <#=Code.Escape(edmFunction)#>(<#=paramList#>)
    {
<#+
        if(returnType != null && (returnType.EdmType.BuiltInTypeKind == BuiltInTypeKind.EntityType ||
                                  returnType.EdmType.BuiltInTypeKind == BuiltInTypeKind.ComplexType))
        {
#>
        ((IObjectContextAdapter)this).ObjectContext.MetadataWorkspace.LoadFromAssembly(typeof(<#=MultiSchemaEscape(returnType)#>).Assembly);

<#+
        }

        foreach (var parameter in parameters.Where(p => p.NeedsLocalVariable))
        {
            var isNotNull = parameter.IsNullableOfT ? parameter.FunctionParameterName + ".HasValue" : parameter.FunctionParameterName + " != null";
            var notNullInit = "new ObjectParameter(\"" + parameter.EsqlParameterName + "\", " + parameter.FunctionParameterName + ")";
            var nullInit = "new ObjectParameter(\"" + parameter.EsqlParameterName + "\", typeof(" + parameter.RawClrTypeName + "))";
#>
        var <#=parameter.LocalVariableName#> = <#=isNotNull#> ?
            <#=notNullInit#> :
            <#=nullInit#>;

<#+
        }

        var genericArg = returnType == null ? "" : "<" + MultiSchemaEscape(returnType) + ">";
        var callParams = Code.StringBefore(", ", String.Join(", ", parameters.Select(p => p.ExecuteParameterName).ToArray()));

        if (includeMergeOption)
        {
            callParams = ", mergeOption" + callParams;
        }
#>
        return ((IObjectContextAdapter)this).ObjectContext.ExecuteFunction<#=genericArg#>("<#=edmFunction.Name#>"<#=callParams#>);
    }
<#+
    if(!includeMergeOption && returnType != null && returnType.EdmType.BuiltInTypeKind == BuiltInTypeKind.EntityType)
    {
        WriteFunctionImport(edmFunction, true);
    }
}

string AccessibilityAndVirtual(string accessibility)
{
    return accessibility + (accessibility != "private" ? " virtual" : "");
}

string MultiSchemaEscape(TypeUsage usage)
{
    var type = usage.EdmType as StructuralType;
    return type != null && type.NamespaceName != ModelNamespace ?
        Code.CreateFullName(Code.EscapeNamespace(type.NamespaceName), Code.Escape(type)) :
        Code.Escape(usage);
}

#>

Für den CodeFirst-Ansatz wird das Ganze schon ein wenig wilder, da das Model zur Laufzeit erstellt wird und der Metadaten-ConnectionString in dieser Form auch nicht existiert. Aber auch hier habe ich mit ein wenig „Basteln“ eine lauffähige Variante hinbekommen. Da dieser Workaround ziemlich wild ist, habe ich diesen als T4-Vorlage realisiert. Folgende Vorteile ergeben sich daraus: Der Workaround ist dokumentiert und lässt sich leichter entfernen, wenn in einer zukünftigen Version der 2nd-Level Cache endlich mal realisiert wird.

Die T4-Vorlage für den wildesten aller Workarounds ist hier:


<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ output extension=".txt" #>
<#@ assembly name="Microsoft.CSharp" #>
<#@ assembly name="System.Data.Entity" #>
<#@ assembly name="$(ProjectDir)$(OutDir)EntityFramework.dll" #>
<#@ assembly name="$(TargetPath)" #> // in this case dll with codefirst context
<#@ import namespace="System.Data.EntityClient" #>
<#@ import namespace="System.Data.Entity.Infrastructure" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Xml" #>
<#@ include file="TemplateFileManager.CS.ttinclude" #>
<#@ include file="VsAutomationHelper.CS.ttinclude" #>
<#@ include file="ConfigurationAccessor.CS.ttinclude" #>

<# 
// ToDo: Initalize an instance of the DbContext (T4 uses connection by convention)
var contextInstance = new DbContextCodeFirst2ndLevelCache.TestContext();

// Change database connection 
// Workaround for accessing the app.config from t4 [random appdomain :-(]
ConfigurationAccessor config = new ConfigurationAccessor((IServiceProvider)this.Host);
// set the connectionstring
string cn = config.ConnectionStrings.Cast<ConnectionStringSettings>()
	.Where(c=>c.Name == contextInstance.GetType().Name).Single().ConnectionString;

contextInstance.Database.Connection.ConnectionString = cn;


string contextName = contextInstance.GetType().Name;
string contextNamespace = contextInstance.GetType().Namespace;
string contextSuffix = "OC";
var fileManager = TemplateFileManager.Create(this);

// File properties for C# output files
var compileProp = new FileProperties();
compileProp.BuildAction = BuildAction.Compile;

// File properties for EDMX output
var edmxProp = new FileProperties();
edmxProp.BuildAction = BuildAction.EntityDeploy;
edmxProp.CustomTool = "EntityModelCodeGenerator";

// Create Edmx file
fileManager.StartNewFile(String.Format("{0}.edmx", contextName)
						, fileProperties:edmxProp);

this.Write(this.GetEdmxFileFromDbContext(contextInstance));

// Create the ObjectContext with ef provider wrapper
fileManager.StartNewFile(String.Format("{0}{1}.cs", contextName, contextSuffix)
						, fileProperties:compileProp);

CreateObjectContextFromDbContext(contextName, contextNamespace, contextSuffix);


fileManager.Process(true);

#>

<#+ 

void CreateObjectContextFromDbContext(string contextName, string contextNamespace, string contextSuffix)
{
#>
namespace <#= contextNamespace #>
{
	using System;
	using System.Collections.Generic;
	using System.Configuration;
	using System.Linq;
	using System.Text;
	using System.Data;
	using System.Data.Entity;
	using System.Data.EntityClient;
	using System.Data.Objects;
	using EFProviderWrapperToolkit;
    using EFTracingProvider;
    using EFCachingProvider;
    using EFCachingProvider.Caching;
	
	public class <#= contextName #><#= contextSuffix #> : ObjectContext
	{
	
	    public <#= contextName #><#= contextSuffix #>()
        	: base (EntityConnectionWrapperUtils.CreateEntityConnectionWithWrappers(
          		TransformToMetaDataConnectionString("<#= contextName #>"),
          			/*"EFTracingProvider",*/
          			"EFCachingProvider"
          		), "<#= contextName #>")
	    {
	    }
		
		public static string TransformToMetaDataConnectionString(string contextName)
		{
		  string folderres = "Workaround."; //Subfolder of workaround with edmx
          var con = System.Configuration.ConfigurationManager.ConnectionStrings
            .Cast<ConnectionStringSettings>()
            .Where(c => c.Name == contextName)
            .Single();

          if (con == null)
          {
            throw new ArgumentException("No connection with name '{0}' found.", contextName);
          }

          var conn = new EntityConnectionStringBuilder();
          conn.ProviderConnectionString = con.ConnectionString;
          conn.Provider = con.ProviderName;
          conn.Metadata = String.Format("res://*/{1}{0}.csdl|res://*/{1}{0}.ssdl|res://*/{1}{0}.msl", contextName, folderres);
          return conn.ConnectionString;
		}
	}
}
<#+ 
}

/// <summary>
/// The code first context must already exists and compiled. Be sure that 
/// the connection string exists in the app.config and the database is created. 
/// Required for edmx file creation
/// </summary
string GetEdmxFileFromDbContext(System.Data.Entity.DbContext ctx)
{	
	string xml = String.Empty;
	
    var sw = new UTF8StringWriter();
    using (var writer = new XmlTextWriter(sw))
    {
      EdmxWriter.WriteEdmx(ctx, writer);
    }
	
    xml = sw.ToString();
    

	return xml;
}

public class UTF8StringWriter : StringWriter
{
  public override Encoding Encoding
  {
	get { return Encoding.UTF8; }
  }
}

#>

Die Beispiel-Solution kann hier heruntergeladen werden. Für die DbFirst bzw. ModelFirst-Variante muss zuerst eine Datenbank mit den Namen DbContextTest angelegt werden, dass SQL-Skript befindet sich im Verzeichnis SQL-Skripts. Der EFCachingProvider in der Solution ist modifiziert, die Anpassungen sind auch in diesem Blog beschrieben.

Zurück

29.06.2011
22:48

T4 - EntityFramworkTemplateFileManager durch den TemplateFileManager ersetzen

Eigene Code-Generierungsvorlagen für das Entity Framework können sehr einfach erstellt werden. Es gilt nur eine Konvention zu beachten. Details dazu gibt es in diesem Beitrag.

In der Basis arbeitet der EntityFrameworkTemplateFileManager, der in Verbindung mit den Code-Generierungsvorlagen häufig zur Anwendung kommt. Die Vorlagen für den Kontext und die Businessobjekte sind in der Regel auf zwei T4-Vorlagen aufgeteilt, damit ein gewisses Mass an Flexiblilät erreicht werden kann. So lässt sich bspw. die Vorlage für die Business-Objekte in eine andere Solution verschieben. Nach der Anpassung des Pfades zur EDMX-Datei funktioniert diese auch wieder. Bei grossen Modellen ist dieser Ansatz auch nicht immer die beste Lösung, da alle Klassen der Vorlage untergeordnet sind und die Generierung in einzelne Verzeichnisse nicht möglich ist. Da der EDM-Designer erweitert werden kann, wie es in diesem Beitrag beschreiben wird, besteht die Möglichkeit, dass für die Klassen ein Package-Namespace definiert werden kann, in dem die Klassen später hinein generiert werden.

Mit dem EntityFrameworkTemplateFileManager ist das jedoch nicht möglich, sodass für die Vorlage ein anderer Lösungsansatz verwendet werden muss. Da der TemplateFileManager auf dem EntityFramworkTemplateFileManager basiert, kann dieser für die Aufgabe herangezogen werden.

Der Austausch erfolgt nach dem klassischen Muster Suchen und Ersetzen. Warum das so ist, erläutere ich nicht noch mal. ;-)

Betrachten wir diesen klassischen Ansatz an der mitgelieferten Code-Generierungsvorlage "Self Tracking Entities". Für die Klassen soll neu die Möglichkeit bestehen, dass diese in eine andere Projektmappe und der entsprechenden Unterverzeichnisse gem. Package-Namespace generiert werden.

Dazu öffne ich die T4-Vorlage für die Klassen.

Abbildung 1
Abbildung 1 zeigt die T4 Vorlage für die Erstellung der Klassen des Modells

Dann füge ich die Include-Datei TemplateFileManager.CS.ttinclude hinzu.

Abbildung 2
Abbildung 2 zeigt die Vorlage mit der hinzugefügten Include-Direktive TemplateFileManager.CS.ttinclude

Anschliessend nutze ich die Tastenkombination Strg + H, um die Vorkommen EntityFrameworkTemplateFileManager durch den TemplateFileManager zu ersetzen.

Abbildung 3
Abbildung 3 Ersetzen des EntityFrameworkTemplateFileManagers durch die klassische Methode Suchen und Ersetzen

Nun können bei der Methode StartNewFile die neu zur Verfügung stehenden benannten Parameter verwendet werden.

Eigentlich ein Aufwand, der sich in Grenzen hält.

Zurück

25.06.2011
03:47

T4 - Wo steht die Version 2 des TemplateFileManager zur Verfügung

Über die neuen Funktionen des TemplateFileManagers habe ich bisher nur geschrieben, öffentlich war dieser nicht. Das hole ich mit diesem Beitrag nach.

Neben der Veröffentlichung in der Code Gallery des tangible T4 Editors steht die neue Version des TemplateFileManagers auch hier zum Download bereit.

Abbildung 1
Abbildung 1 TemplateFileManager im Bereich .ttinclude der Code Gallery des tangible T4-Editors

In den Beiträgen mit dem Tag TemplateFileManager gibt es weitere Infos.

Zurück

21.06.2011
21:58

T4 - Benutzerdefinierte Parameter für die StartHeader-Methode im TemplateFileManager

Wie der einzige Standardparameter $filename$ verwendet werden kann, ist in diesem Beitrag beschrieben. Im Header können wiederkehrende Teile wie zum Beispiel der Dateikopf bzw. die Namensraumdefinition hinterlegt werden.

Der Standardparameter wird dafür sicherlich nicht ausreichen und so können über die FileProperties weitere benutzerdefinierte Parameter festgelegt werden.

Folgendes Szenario soll die Verwendung veranschaulichen.

Im Bereich, der durch die Methode StartHeader festgelegt wird, sollen pro Datei weitere Parameter für den Namensraum und den Klassennamen verwendet werden. Der Aufbau könnte wie folgt aussehen:


var fileManager = TemplateFileManager.Create(this);
fileManager.IsAutoIndentEnabled = true;

fileManager.StartHeader();
#>
// <copyright file="$filename$" company="Databinding">
//     Copyright (c) databinding. 
// </copyright>
namespace $namespace$
{
  /// <summary>
  /// Represents the $class$.
  /// </summary>
<# 
fileManager.EndBlock();
fileManager.StartFooter();
#>
}
<#
fileManager.EndBlock();

Die neu definierten Parameter $namespace$ und $class$ müssen in diesem Fall über die StartNewFile-Methode dem benannten Parameter fileProperties übergeben werden. Der Aufruf würde dabei wie im folgenden Codeausschnitt aussehen:


fileManager.StartNewFile("HeaderTest1.cs"
	, fileProperties:GetFileProperties("App.Administration", "HeaderTest1"));
this.WriteClass("HeaderTest1");

fileManager.StartNewFile("HeaderTest2.cs"
	, fileProperties:GetFileProperties("App.Business", "HeaderTest2"));
this.WriteClass("HeaderTest2");

fileManager.Process();
#>
<#+ 

FileProperties GetFileProperties(string namespaceName, string className)
{
	FileProperties fp = new FileProperties();
	fp.BuildAction = BuildAction.Compile;
	fp.TemplateParameter.Add("$namespace$", namespaceName);
	fp.TemplateParameter.Add("$class$", className);
	
	return fp;
}

void WriteClass(string className)
{
#>
public class <#= className #>
{
}
<#+ 
}
#>

Bei der Erstellung werden die benutzerdefinierten Parameter ersetzt und folgendes Resultat erzeugt:


// <copyright file="HeaderTest1.cs" company="Databinding">
//     Copyright (c) databinding. 
// </copyright>
namespace App.Administration
{
  /// <summary>
  /// Represents the HeaderTest1.
  /// </summary>
  public class HeaderTest1
  {
  }
}

Dieser Ansatz ist einer von vielen und mich interessiert hier primär, ob die Methode StartHeader durch Verwendung von Parametern bei mir häufiger zum Einsatz kommen wird. ;-)

Zurück

16.06.2011
23:18

T4 - Custom Tool ein weiteres neues Feature im TemplateFileManager

Custom Tool, dabei handelt es sich um eine Komponente für Visual Studio mit der eine dem Projektitem untergeordnete Quellcode-Datei erstellt werden kann. Für T4-Templates zum Beispiel heisst das Custom Tool TextTemplatingFileGenerator und ist für den Output zuständig (Im Fall von T4 die Ausgabe anstossen).

Es gibt Situationen, da ist es richtig praktisch, wenn bei der Codegenerierung so ein Custom Tool verwendet werden kann. In meinen Beispielen der ADC 2010 befindet sich dazu ein Szenario für Ressource-Dateien. In einem Beispiel benutze ich meinen VsAutomationHelper, um nach der Fertigstellung der Generierung das Custom Tool auf die Standardressource zu setzen.

Der Vorteil sollte eigentlich jedem ersichtlich werden. Ich spare mir Zeit, in dem ich keine Templates schreibe, wofür in Visual Studio bereits ein fertiges Konzept besteht.

Das Custom Tool habe ich bisher mit den AutomationHelper mit folgender Zeile gesetzt:


// Enable strong typed resource
this.dteHelper.SetCustomToolForGeneratedItem("Resource.resx", "ResXFileCodeGenerator");

Neu kann der TemplateFileManager diese Informationen mit der Methode StartNewFile entgegen nehmen.

Betrachten wir das Feature anhand folgendem Szenario:

Ich habe eine Excel-Datei, welche die mehrsprachigen Ressourcen für die Anwendung enthält. Dies wird zum Beispiel dann notwendig, wenn die Angaben durch einen Übersetzer validiert werden sollen. Diese Anwender arbeiten bevorzugt mit Anwendungen wie zum Beispiel Office und mit Excel habe ich dann eine strukturierte Vorlage, die im nächsten Schritt wieder verarbeitet werden kann. So können auch Medienbrüche minimiert werden.

Abbildung 1
Abbildung 1 zeigt den exemplarischen Aufbau der Excel-Datei

Damit haben wir die Quelle zur Erstellung der Ressourcen. In T4 benötige ich noch eine Basisvorlage für das typische XML. Dazu nutze ich ein Parameter-Template welches die Metadaten entgegen nimmt.


<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ output extension=".txt" #>
<#@ assembly name="C:\tt\ExcelHelper.dll" #>
<#@ parameter name="Document" type="ExcelHelper.ResourceDocument" #>
<#@ parameter name="CultureKey" type="System.String" #>
<# 
// Woraround for parameter
// ExcelHelper.ResourceDocument Document;
if (Document == null)
{
	throw new ArgumentNullException("Document", "The processing of the Template was aborted."); 
}

if (String.IsNullOrEmpty(CultureKey))
{
	throw new ArgumentNullException("CultureKey", "The processing of the Template was aborted. The key was not set to en, de, fr or it."); 
}
#>
<?xml version="1.0" encoding="utf-8"?>
<root>
  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
    <xsd:element name="root" msdata:IsDataSet="true">
      <xsd:complexType>
        <xsd:choice maxOccurs="unbounded">
          <xsd:element name="data">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="resheader">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required" />
            </xsd:complexType>
          </xsd:element>
        </xsd:choice>
      </xsd:complexType>
    </xsd:element>
  </xsd:schema>
  <resheader name="resmimetype">
    <value>text/microsoft-resx</value>
  </resheader>
  <resheader name="version">
    <value>1.3</value>
  </resheader>
  <resheader name="reader">
    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <resheader name="writer">
    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
<# foreach (ExcelHelper.Table item in Document.Table)
{#>
	<data name="<#= item.ResourceName #>" xml:space="preserve">
    <value><#= GetContentForCultureKey(CultureKey, item) #></value>
    <comment></comment>
  </data>	 
<#} #>
</root>
<#+  
private string GetContentForCultureKey(string cultureKey, ExcelHelper.Table item)
{
	switch (cultureKey.ToLower())
	{
		case "de":
			return item.Language.De;
		case "fr":
			return item.Language.Fr;
		case "it":
			return item.Language.It;
		default:
			return item.Language.En;
	}
}
#>

Nun geht es darum, mit dem TemplateFileManager die vier Ressource-Dateien zu erstellen.

Der notwendige Code dafür sieht so aus:


string[] cultureKeyArray = new string[] {"en", "de", "fr", "it"};
var fm = TemplateFileManager.Create(this);
ResourceSheet sheet = new ExcelHelper.ResourceSheet();
sheet.Load(@"C:\tt\T4Enhanced.xlsx");

for (int idx = 0; idx < cultureKeyArray.Length; idx++)
{
	string key = cultureKeyArray[idx];
	fm.StartNewFile("Resource" + this.GetResourceExtension(key));
	this.Write(this.GenerateResourceWithTemplate("ResourceTemplate.tt", key, sheet.Document));
}

fm.Process(true);

Wenn ich damit die Ressource-Dateien erstelle, so habe ich nur die einfachen resx-Dateien.

Abbildung 2
Abbildung 2 Erstellung der Ressource-Dateien ohne Zuordnung eines Custom Tools

Die zugeordnete Designerdatei für die Standard-Ressource fehlt. Wie bereits erwähnt, gibt es mit den VsAutomationHelper die Möglichkeit ein Custom Tool zu definieren.

Die StartNewFile - Methode hat neu einen benannten Parameter fileProperties. Dieser ermöglicht die Übergabe eines Custom Tools. Es muss nur geprüft werden, welche Kultur für die Standard-Ressource vorgesehen ist. In diesem Fall nehmen wir "en".

Der angepasste Code hat nun folgenden Aufbau:


string[] cultureKeyArray = new string[] {"en", "de", "fr", "it"};
var fm = TemplateFileManager.Create(this);
ResourceSheet sheet = new ExcelHelper.ResourceSheet();
sheet.Load(@"C:\tt\T4Enhanced.xlsx");

var fp = new FileProperties();
fp.CustomTool = "ResXFileCodeGenerator";

for (int idx = 0; idx < cultureKeyArray.Length; idx++)
{
	string key = cultureKeyArray[idx];
	if (key == "en")
		fm.StartNewFile("Resource" + this.GetResourceExtension(key), fileProperties:fp);
	else
		fm.StartNewFile("Resource" + this.GetResourceExtension(key));
	
	this.Write(this.GenerateResourceWithTemplate("ResourceTemplate.tt", key, sheet.Document));
}

fm.Process(true);

Schaue ich nun in den Solution-Explorer, so wird für die Standard-Ressource auch die Designerdatei erzeugt.

Abbildung 3
Abbildung 3 Standard-Ressource mit Designerdatei

Ein weiterer Vorteil: Bedingt durch die Nutzung der bestehenden Infrastruktur werden auch die notwendigen Assemblies pro Sprache erstellt.

Wenn ich allerdings dieses Beispiel zeige, dann höre ich gelegentlich das Argument: Wir können das nicht so machen, wir speichern die Werte schliesslich in der Datenbank.

Wenn Du jetzt auch so denkst, ab .NET 2.0 ist das Framwork ein guter Baukasten geworden. Es ist ohne Weiteres möglich einen eigenen ResourceProvider zu schreiben und diesen im Config-File zu registrieren.

So können die definierten Werte auch in der Datenbank gespeichert werden und die Nutzung der Visual Studio-Infrastrukur bleibt bestehen.

Wäre in diesem Fall einen Versuch wert, wenn man bedenkt, dass dieser Ansatz auch mit den DataAnnotations verwendet werden kann, oder?

Zurück

Translate this page

Kategorien

  • [-].NET Development (215)
  • [-]Datenbank (26)
  • HTML (1)
  • Konfiguration (12)
  • Mind Map (10)
  • Off-topic (9)
  • Open Source (3)
  • Qualität (7)
  • Sharepoint (6)
  • Sicherheit (2)

Archiv

Social Bookmarking

Bookmark bei: Mr. Wong Bookmark bei: Webnews Bookmark bei: Icio Bookmark bei: Oneview Bookmark bei: Linkarena Bookmark bei: Favoriten Bookmark bei: Seekxl Bookmark bei: Favit Bookmark bei: Social Bookmarking Tool Bookmark bei: Power Oldie Bookmark bei: Bookmarks.cc Bookmark bei: Newskick Bookmark bei: Newsider Bookmark bei: Linksilo Bookmark bei: Readster Bookmark bei: Folkd Bookmark bei: Yigg Bookmark bei: Digg Bookmark bei: Del.icio.us Bookmark bei: Reddit Bookmark bei: Simpy Bookmark bei: StumbleUpon Bookmark bei: Slashdot Bookmark bei: Netscape Bookmark bei: Furl Bookmark bei: Yahoo Bookmark bei: Spurl Bookmark bei: Google Bookmark bei: Blinklist Bookmark bei: Blogmarks Bookmark bei: Diigo Bookmark bei: Technorati Bookmark bei: Newsvine Bookmark bei: Blinkbits Bookmark bei: Ma.Gnolia Bookmark bei: Smarking Bookmark bei: Netvouz Information