Laden...

[Artikel] Resourcen in .NET Assemblies

Erstellt von egrath vor 17 Jahren Letzter Beitrag vor 17 Jahren 41.171 Views
egrath Themenstarter:in
871 Beiträge seit 2005
vor 17 Jahren
[Artikel] Resourcen in .NET Assemblies

Verwendung von Resourcen in .NET Assemblies

Vorwort

In diesem Artikel werden wir uns die verwendung von Resourcen unter .NETansehen. Viele benutzen diese schon seit langem indem einfach im Visual Studio die entsprechende Datei eingefügt und "benutzt" wird. Dass hinter all dem aber viel IDE-Magic steckt und im hintergrund schon eine ganze Menge Code erforderlich sein kann damit das so schön funktioniert wird man sehen wenn man diesen Artikel fertig gelesen hat.

Überblick

Resourcen sind in Assemblies (genauer gesagt in den Metadaten) eingebettete Dateien welche zur Laufzeit gelesen und verarbeitet werden können. Diese Daten können für verschiedene Zwecke genutzt werden - dies sind zum beispiel:*Lokalisierung der in der Applikation benutzten, textuellen Informationen (beispielsweise Benutzer Texte) *Grafiken und andere Daten für die Applikation.

Grundsätzlich gibt es zwei verschiedene Typen von Resourcen in Assemblies:*Direkt eingebundene
Eine Datei wird direkt in die Assembly gelinkt und kann benutzt werden.

*Gesammelt in einer ".resources" Datei
Der Zugriff erfolgt über eine spezielle Klasse welche sich auch um die Lokalisierung kümmert.

Grafisch gesehen sieht dies also so aus:


_Abb. 1: Wie können Resourcen in Assemblys gespeichert sein? _

Beiden Arten von Resourcen können beim kompilieren der Applikation angegeben werden. Dies geschieht mit dem Paramter "/resource", welcher als Parameter die entsprechende Datei entgegebennimmt.

Das erstellen von Resource Dateien

Bei direkt eingebundenen Dateien stellt sich die Frage der erstellung per Definition nicht, da diese üblicherweise mit einer externen Applikation wie Bild- oder Tonverarbeitung erstellt werden. Die ".resources" Dateien hingegen können mit verschiedenen, .NET eigenen Bordmitteln erstellt werden:*Das Tool "resgen" *Die Klasse "ResourceWriter" des Frameworks

Beginnen wir mit dem Tool ResGen. Als Parameter nimmt dieses Tool verschiedene Dateien entgegeben, welche die notwendigen Informationen enthalten. Dies sind:*txt
Jede Zeile der Textdatei repräsentiert ein Key/Value Pair welche durch das '=' Zeichen getrennt sind. Das kann zum beispiel dafür benutzt werden um lokalisierungs-spezifische Texte als Resource aufzunehmen.

*resx
Eine XML Darstellung von Resourcen. Mit diesem Format können neben Textuellen Daten auch Binärdaten (wie bsp. Image) als Resource gespeichert werden. Leider ist im .NET SDK kein Tool enthalten mit dem ResX Dateien direkt editiert werden können. Zum Glück ist die Freeware-Szene hier in die Presche gesprungen und Lutz Roeder hat mit dem "Resourcer" ein entsprechendes Tool zur verfügung gestellt.

Zur Verdeutlichung erstellen wir uns die beiden folgenden Dateien:

AppStrings.en-US.txt:


strWelcome = Welcome to our Application
strEnter = Enter your Name

AppStrings.de-DE.txt:


strWelcome = Willkommen in der Applikation
strEnter = Bitte geben Sie Ihren Namen ein

Diese beiden Dateien werden nun mittels ResGen in die entsprechenden "resources" Dateien übersetzt ("resgen AppStrings.de-DE.txt" und "resgen AppStrings.en-US.txt"). Als ergebnis erhalten wir nun die beiden Dateien "AppStrings.de-DE.resources" und "AppStrings.en-US.resources". Diese könnten wir nun schon entweder direkt in unsere Applikation einbetten oder aber auch eine neue DLL erstellen welche ausschliesslich diese Resourcen enthält. Gehen wir aber einen Schritt weiter. Wir wollen jetzt noch zusätzlich zwei Grafiken in die Resources Datei einfügen. Dazu bemühen wir den Resourcer und laden das entsprechende .resources File:


Abb. 2: Der Resourcer mit dem geladenen AppStrings.de-DE.resources File

Um nun auch noch andere Daten einzufügen reicht es im Menü "Edit" den Punk "Insert File" (respektive "Insert Text" für String Resourcen) auszuwählen und die entsprechende Datei einzufügen. Wird der Typ der Datei nativ vom Framework unterstützt, so wird dieser in Serialisierter Form des entsprechenden Objekts gespeichert, anderenfalls als Byte-Array. Nachdem zum beispiel ein Bild und eine unbekannte Datei eingefügt worden sind, sieht dies so aus:


Abb. 3: Eine .resources Datei mit einigen eingefügten Daten.

Es muss erwähnt werden, dass man sich das Tool ResGen und die ResX Dateien komplett ersparen kann, da der Resourcer in der Lage ist Resources Dateien komplett neu zu erstellen und diese in einem Format beliebiger Wahl speichern kann.
*Lokalisierbare Resourcen
Kommen in eigene DLL's, da dies die übersichtlichkeit und einfache Lokalisierung durch Fremdanbieter ermöglicht (Es muss nur die entsprechende DLL weitergegeben werden). Der Grund hierfür ist, dass zusätzliche Sprachen nachträglich einfach eingefügt werden können, ohne dass man die Hauptapplikation neu verteilen muss.

*Nicht-lokalisierbare Resourcen
Sollten in die eigentliche Hauptapplikation eingebettet werden. Darunter fallen z.b. Grafiken welche innerhalb der Applikation benutzt werden oder aber auch textuelle Informationen die nichts mit Benuzter-Interaktion zu tun haben (SQL Statements). Es spricht natürlich nichts dagegen wenn man sich auch für diese Resourcen eine eigene DLL erstellt - ist meiner persönlichen Meinung nach nur eine reine Geschmackssache.

Um eine Resourcen-DLL zu erstellen (die ja nur Resourcen und keinen Programmcode enthält) reicht im Endeffekt der folgende Kompileraufruf:


csc /target:library /out:resources.dll /resource:AppStrings.en-US.resources /resource:AppStrings.de-DE.resources /resource:Images.resources 

Sieht man sich nun diese DLL im ILDasm an, so erkennt man deutlich dass die Resourcen wie oben bereits erwähnt im Manifest gespeichert wurden:


Abb. 4: Resourcen in einer DLL, gespeichert im Manifest

Bei der Einbindung in die Hauptapplikation ist es lediglich notwendig über den Kompilerparameter "/resource:<Dateiname>" anzugeben, welches Resources File in das Manifest aufgenommen werden soll.

Benutzung von Resourcen

Resourcen können wie oben erwähnt mittels direkten Zugriff auf den Stream der Datei, oder aber über den ResourceManager angesprochen werden. Zuerst sehen wir uns an, wie dies mittels des Resource Managers funktioniert - da dieser Lokalisierungseigenschaften bietet ist dieser die Primäre Wahl beim erstellen von lokalisierten Applikationen:


ResourceManager locRm = new ResourceManager( "AppStrings", Assembly.LoadFile( "resources.dll" ));
Console.Out.WriteLine( "strHello = {0}", locRm.GetString( "strHello", new CultureInfo( locale )));

Im Endeffekt beschränkt sich das ganze darauf, dass wir ein Objekt vom Typ ResourceManager instanziieren dem wir als Konstruktor-Parameter folgendes übergeben:
*Den Basisnamen der eingebetteten Resources Datei. Da bei uns die Resources Dateien für die unterschiedlichen Sprachen alle vom Format "AppStrings.<LangCode>.resources" sind, ist dieser Basisname "AppStrings" *Die Assembly in welcher sich die Resourcen befinden

Anschliessend lassen wir uns vom ResourceManager immer jene Strings zurückgeben, welche wir mittels des Parameters "CultureInfo" anfordern. Der Parameter "locale" ist vom Typ "String" und hat als inhalt die entsprechende Lokalität (bsp. en-US oder de-DE ins unserem Fall). Eine komplette Liste der Locales kann im MSDN unter diesem Link nachgeschlagen werden.

Neben der möglichkeit sich Strings aus der Resource zu holen, kann der ResourceManager uns natürlich auch Streams oder andere Objekte zurückgeben. Die entsprechenden Methoden heissen "GetStream" und "GetObject" und haben die gleichen Eingangsparamter wie "GetString".

Kommen wir nun zum lesen einer Resource direkt als Stream. Diese Methode ist nicht so gebräuchlich, wird allerdings trotzdem unterstützt und hat für bestimmte Einsatzzwecke auf jeden Fall Ihre Berechtigung. So ist es sicherlich sinnvoller bei einer relativ grossen Datei welche eingebettet werden soll dies nicht über den ResourceManager zu machen sondern direkt über Streams - der Vorteil liegt in Bezug auf Performance da der ResourceManager einen gewissen Overhead erzeugt.

Ein Assembly Objekt besitzt zwei für uns interessante Methoden:*Stream GetManifestResourceStream( string name )
Liefert einen Stream zurück, mit welchem die Resource gelesen werden kann

  • string[] GetManifestResourceNames()
    Liefert ein String Array zurück, in dem alle Namen aller eingebetteten Top-Level Resourcen vorliegen

IDE-Magic


_Abb. 5: Warum kann in VS so einfach auf den im Resource File "AppResources" enthaltenen String "testString" zugegriffen werden? _

Wie oben bereits erwähnt ist es mit Visual Studio relativ leicht, eigene Resourcen in die Applikation einzubinden und auf diese auch noch relativ unproblematisch zuzugreifen. Wenn man im VS ein neues Resource File (ResX) erstellt, so geschieht folgendes:*Das ResX File wird erstellt und VS bietet einen grafischen Dialog zum bearbeiten von darin enthaltenen Resourcen (wird beim Kompilieren in ein Resources File umgewandelt und gelinkt) *Dem Projekt wird ein neues Quellcode-File hinzugefügt (als Child des ResX Files) welches die notwendige Programmlogik enthält um auf die Elemente dieser Resource direkt zugreifen zu können.

Sehen wir uns einen kleinen ausschnitt aus dem von VS erstellen File an:


/// <summary>
///   Looks up a localized string similar to Hello World.
/// </summary>
internal static string testString {
    get {
        return ResourceManager.GetString("testString", resourceCulture);
    }
}

Wie man erkennen kann macht VS im Endeffekt auch nichts anderes als wir oben und gibt mittels des ResourceManagers die angeforderten Daten an den Aufrufer zurück. Damit ist auch dieses Mysterium von IDE-Magic aufgelöst.

Quellcode


using System;
using System.Reflection;
using System.IO;
using System.Collections;
using System.Resources;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.Serialization.Formatters.Binary;

namespace egrath.test
{
        public class Program
        {
                public static void Main( string[] args )
                {
                        Assembly al = Assembly.GetExecutingAssembly();

                        if( args.Length > 0 )
                        {
                                al = Assembly.LoadFile( args[0] );
                        }

                        ResourceDumper dumper = new ResourceDumper( al );
                        Console.Out.WriteLine( "Dumped {0} files", dumper.DumpResources( String.Empty ));
                }
        }

        internal class ResourceDumper
        {
                private Assembly m_DumpAssembly;

                public ResourceDumper( Assembly al )
                {
                        m_DumpAssembly = al;
                }

                public int DumpResources( string filter )
                {
                        int dumped = 0;

            string outputDir = "dumped";
            if( ! Directory.Exists( outputDir )) Directory.CreateDirectory( outputDir );

                        string[] resourceNames = m_DumpAssembly.GetManifestResourceNames();
                        foreach( string resource in resourceNames )
                        {
                                if( resource.EndsWith( ".resources" )) // It's a embedded Resource file which itself can contain resources
                                {
                                        Console.Out.WriteLine( "examining embedded resource: [{0}]", resource );
                                        ResourceReader resReader = new ResourceReader( m_DumpAssembly.GetManifestResourceStream( resource ));
                                        IDictionaryEnumerator resEnum = resReader.GetEnumerator();
                                        while( resEnum.MoveNext() )
                                        {
                                                string resourceType = String.Empty;
                                                byte[] resourceData;

                                                resReader.GetResourceData(( string ) resEnum.Key, out resourceType, out resourceData );
                        resourceType = resourceType.Split( ',' )[0];
                                                Console.Out.WriteLine( "    dumping resource: name={0}, type={1}", resEnum.Key, resourceType );

                        byte[] fileData = ConvertToFile( resourceType, resourceData );

                        BinaryWriter writer = new BinaryWriter( new StreamWriter( String.Format( "{0}/{1}.{2}", outputDir, resEnum.Key, GetExtensionForType( resourceType ))).BaseStream );                        
                        writer.Write( fileData );
                        writer.BaseStream.Close();
                        writer.Close();

                        dumped ++;
                                        }
                                }
                                else
                                {
                                        Console.Out.WriteLine( "Dumping embedded Resource: [{0}]", resource );
                    BinaryWriter writer = new BinaryWriter( new StreamWriter( String.Format( "{0}/{1}", outputDir, resource )).BaseStream );

                    Stream resourceStream = m_DumpAssembly.GetManifestResourceStream( resource );
                    int byteCode = -1;
                    while(( byteCode = resourceStream.ReadByte() ) != -1 )
                    {
                        writer.Write(( byte ) byteCode );
                    }
                    writer.BaseStream.Close();
                    writer.Close();

                    dumped ++;
                                }
                        }

                        return dumped;
                }

        private string GetExtensionForType( string type )
        {
            switch( type )
            {
                case "System.Drawing.Bitmap": return "bmp";
                case "System.Drawing.Icon": return "ico";
                case "ResourceTypeCode.String": return "txt";
                default: return "unknown";
            }
        }

        private byte[] ConvertToFile( string resourceType, byte[] resourceData )
        {
            byte[] fileData;
            BinaryFormatter formatter = null;

            switch( resourceType )
            {
                case "System.Drawing.Bitmap":
                    formatter = new BinaryFormatter();
                    Image img = ( Image ) formatter.Deserialize( new MemoryStream( resourceData ));
                    MemoryStream imageStream = new MemoryStream();
                    img.Save( imageStream, ImageFormat.Bmp );
                    fileData = imageStream.ToArray();
                    break;

                case "System.Drawing.Icon":
                    formatter = new BinaryFormatter();
                    Icon ico = ( Icon ) formatter.Deserialize( new MemoryStream( resourceData ));
                    MemoryStream iconStream = new MemoryStream();
                    ico.Save( iconStream );
                    fileData = iconStream.ToArray();
                    break;

                default:
                    fileData = new byte[resourceData.Length];
                    Array.Copy( resourceData, fileData, resourceData.Length );
                    break;
            }

            return fileData;
        }
        }
}