21 - 12 - 2024

Multilinguale WPF-Resourcen bei gleichzeitiger Obfuscation

Bewertung:  / 2
SchwachSuper 

MyWpfMainApp.Helpers.StringsHelperDiesmal berichte ich über meine gemachten Erfahrung im Zusammenhang mit der Nutzung von String-Resourcen während der Entwicklung einer mehrsprachigen WPF-Anwendung bei gleichzeitiger Nutzung von Obfuscation-Tools.

Ich nahm insesgamt vier Anläufe bis ich eine Lösung praktikable gefunden hatte. Wer ebenfalls auf der Suche nach einer Lösung ist und eine der 4 Lösungen hier im Internet findet kann sich informieren, mit welchen Vorteile bzw. auch Pferdefüsse die einzelnen Lösungen so bereithalten.

Für die Entwicklung einer Applikation hat Microsoft im Visual Studio einen excellent, funktionierenden Resourcen-Manager integriert, der es dem Programmierer ermöglicht seine String-Resourcen in einer Tabelle je Sprache zu pflegen und der gesteuert über die CultureInfo des jeweiligen Threads dan automatisch die String-Resource der aktuellen Culture des Threads zurückliefert.

Ein Beispiel:
Ich lege in meinem Projekt eine neue Resourcen-Datei "Strings.resx" an und füge dieser die neue String-Resource "MyHelloWorldTitle" = "Hello World!" hinzu. Anschließend erzeuge ich eine weitere Resourcen-Datei "Strings.de.resx" und füge die gleiche String-Resource in deutischer Sprache hinzu: "MyHelloWorldTitle" = "Hallo Welt!". Nun kann ich im C#-Programmcodeauf die String-Resource zugreifen mit:

	Console.WriteLine( Strings.MyHelloWorldTitle );

Je nach dem ob die Ländereinstellungen des gerade laufenden Threads nun auf eine deutsprachigen Cultures ("de-*") oder eine englischesprachige Culture eingestellt sind gibt derselbe Code einmal "Hallo Welt!" und das andere mal "Hello World!" aus.. Soweit ist das erstmal nichts Neues. Nahezu jeder C#-Programmierer kennt und nutzt diesen Resourcen-Manager sobald die Notwendigkeit für mehrsprachige Applikation gegeben ist.

Seit Einführung der Windows Presentation Foundation, kurz WPF, stellt sich dann natürlich auch die Frage, wie man denn auch in xaml-Code analog zum C#-Code auf eine ähnlich elegante Art und Weise auf mehrsprachige String-Resourcen zugreifen kann. Eigentlich geht das zunächst auch problemlos (dachte ich!) auf die gleiche Art, wie in C#. Der Programmcode "Strings-MyHelloWorldTitle" von dem Beispiel oben ist ja nun auch nichts anderes als eine static-Property in der Klasse Strings, die der Visual Studio Designer aus den RESX-Dateien automatisch generiert hatte. Man muss also auch in xaml nur den Namespace der Strings-Klasse deklarieren, die die static-Property direkt aufrufen:

<Window x:class="MyWpfMainApp.MainWindow" 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
        xmlns:p="clr-namespace:MyWpfMainApp.Properties">
    <Grid>
        <Textblock text="{x:Static p:Strings.MyHelloWorldTitle}">
   </Grid>
</Window>

Das funktioniert wirklich prima - auch mehrsprachig! Allerdings nur solange der Programmcode im Debug-Mode läuft. Sobald ich den Programmcode im Release-Mode übersetzte, kommen bei mir Obfuscation-Tools zum Einsatz, die verhindern sollen, dass der Programmcode von Hackern einfach wieder in Quellcode zurückgewandelt und modifiziert werden kann. Dann nämlich meldet das startenden Programm, dass es eine WPF-Resource mit dem Name "MyHelloWorldTitle" nicht finden könne.

Anlauf 1. - Klassische Methode

Ja das war mein erster Anlauf. Also genau so wie in C# geht es nicht. Offenkundig greift das .NET-Framework beim Parsen des xaml-Codes auf die Strings-Resourcen zu, weshalb diese zwingend Public deklariert sein müssen. Und alles was Public deklariert wird, wird von den Obfuscation-Tools verschleiert, so dass das .NET-Framework die Strings-Resourcen nicht mehr findet.

Anlauf 2. - Obfuscation für Strings-Klasse vermeiden

Am naheliegendsten wäre es dann die Obfuscation selektiv nur für die Strings-Resourcen zu deaktivieren während sie für den restlichen Teil der Anwendung aktiv bleibt. Allerdings bot das von mir genutzt Obfuscation-Tool keine Konfigurations-Möglichkeit um Applikations-Resourcen von einem zentralen Ort in dem Projekt (z.B. den AssemblyInfos.cs) aus zu steuern. Obfuscation läßt sich für einzelne Klassen, Methoden oder Properties direkt über die entsprechenden Attribute steuern.

So könnte direkt in der Strings-Klasse das Attribute [ObfuscationAttribute] deklariert werden und so vermieden werden, dass die Klasse verschleiert würde. Das würde dann etwa so aussehen:

namespace MyWpfMainApp.Properties.Strings {
using System;
     [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
	 [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
	 [ObfuscationAttribute(Feature = "renaming", ApplyToMembers = true)]
	 public class Strings {
	 
	      private static global::System.Resources.ResourceManager resourceMan;
		  
		  private static global::System.Globalization.CultureInfo resourceCulture;
		  
		  internal Strings() {
		  }
		  
		  /// <summary>
		  /// Returns the cached ResourceManager instance used by this class.
		  /// </summary>
      [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
		 public static global::System.Resources.ResourceManager ResourceManager
		 {
		      get {
			        if (object.ReferenceEquals(resourceMan, null)) {
					     global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("DynDNSService.Program.Properties.Strings", typeof(Strings).Assembly);
						 resourceMan = temp;
				    }
					
					return resourceMan;
			  }
			  
	     }
		 
		 /// <summary>
		 /// Overrides the current thread's CurrentUICulture property for all
		 /// resource lookups using this strongly typed resource class.
		 /// </summary>
		 [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
		 public static global::System.Globalization.CultureInfo Culture {
		       get {
			        return resourceCulture;
			   }
			   set {
			        resourceCulture = value;
		       }
		 }
		 
		 /// <summary>
		 /// Looks up a localized string similar to Hello World!
		 /// </summary>
		 public static string MyHelloWorldTitle {
		       get {
			        return ResourceManager.GetString("Hello World!", resourceCulture);
			   }
	     }
    }
}

Dumm ist nur, dass diese Klasse nunmal von Visual Studio Designer verwaltet wird, der seinerseits solchen Extracode bei jedem Editieren der Resx-Dateien wieder entfernen würde. Also fällt diese Möglichkeit auch weg.

Anlauf 3. - Deklaration der Strings-Klasse im xaml-Code

Im dritten Anlauf habe ich dann die Strings-Klasse zusammen mit anderen WPF-Resourcen bereits in der App.xaml selbst deklariert und greife dann per Binding auf die static-Properties zu. Dazu wird als 1. die Deklaration in der App.xaml erledigt:

<Application x:class="MyWpfMainApp.App" 
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
             xmlns:p="clr-namespace:MyWpfMainApp.Properties"
			 StartUp="OnStartup">
     <Application.Resources>
       <ResourceDictionary>
		  <ResourceDictionary.MergedDictionaries>
		     <ResourceDictionary Source="pack://application:,,,/Themes/Generic.xaml" />
		  </ResourceDictionary.MergedDictionaries>
	   </ResourceDictionary>
		
	   <p:Strings x:Key="Strings" />
	   
	 </Application.Resources>
</Application>

Sobald das erledigt ist, kann man in allen xaml-Dateien der Applikation auf die neuen Resourcen mit dem Key "Strings" per Binding zugreifen:

<Window x:class="MyWpfMainApp.MainWindow" 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Grid>
        <Textblock text="{Binding MyHelloWorldTitle, Source={StaticResource Strings}}">
   </Grid>
</Window>

Nun, auch das funktioniert hervorragend, allerdings nicht auf Anhieb! Wie man in dem Beispiel der Strings-Klasse oben schon sehen konnte, legt der Visual Studio Designer die Strings-Klasse zwar als Public-Klasse an (das hatte ich per Modifier in der Resx-Datei so festgelegt), dann allerdings wird der parameterlose Konstruktor der Klasse dennoch mit dem Attribute "internal" deklariert. Der Konstruktor müsste daher manuell auf "public" abgeändert werde woraufhin der Konstruktor dann - gegenüber dem Beispiel oben - so aussehen würde:

                        public Strings() {
                  }

Nun funktioniert die Anwendung perfekt - auch mehrsprachig. Allerdings nur solange bis man in einer der Resx-Dateien eine Änderung vornimmt! Dann nämlich ist der Visual Studio Designer wieder am Zug und legt den parameterlosen Strings-Konstruktor wieder "internal" an. Uff!

Anlauf 4. - Implementation einer Helper-Klasse StringsHelper

Schlußendlich habe ich dem Projekt eine weitere Helper-Klasse "MyWpfMainApp.Helpers.StringsHelper" hinzugefügt:

using MyWpfMainApp.Properties;
namespace MyWpfMainApp.Helpers
{
     public sealed class StringsHelper
	 { 
             public Strings Strings;
             {
			      get { return strings; }
				  set {  } 
             }
			 
			 private Strings strings = new Strings();
      }
}

und in der App.xaml instantiiert:

<Application x:class="MyWpfMainApp.App" 
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
             xmlns:hlp="clr-namespace:MyWpfMainApp.Helper"
			 StartUp="OnStartup">
    <Application.Resources>
       <ResourceDictionary>
		   <ResourceDictionary.MergedDictionaries>
			   <ResourceDictionary Source="pack://application:,,,/Themes/Generic.xaml" />
		   </ResourceDictionary.MergedDictionaries>
	   </ResourceDictionary>
		
	   <hlp:StringsHelper x:Key="Strings" />
	   
	</Application.Resources>
</Application>

Nun läßt sich bequem in jeder xaml-Datei auf die Strings-Resourcen zugreifen, wie folgt:

<Window x:class="MyWpfMainApp.MainWindow" 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
        xmlns:p="clr-namespace:MyWpfMainApp.Properties">
    <Grid>
        <Textblock text="{Binding Strings.MyHelloWorldTitle, Source={StaticResource Strings}}">
   </Grid>
</Window>

ohne dass die Obfuscation oder der Visual Studio Designer dazwischenfunken. Schade ist, dass die Strings-Resourcen weiterhin mit dem Modifier "public " deklariert werden müssen, denn es ist ja nach wie vor das .NET Framework, dass diese Resourcen beim parsen benötigt und von außerhalb auf die Assembly zugreift. Ich hätte schon gerne den Modifier "internal" angewandt, denn die Strings-Resourcen werden sonst außerhalb der Assembly eigentlich nicht benötigt.