code it

Martins Tech Blog

Lesen und Schreiben von XML-Dateien mit LINQ to XML

Nachdem ich mich in den letzten Einträgen zum Thema LINQ eher mit dem Thema LINQ to Objects beschäftigt habe, möchte ich nun auch das Thema LINQ to XML anschneiden, denn auch beim Zugriff auf XML-Daten bietet LINQ den ein oder anderen Vorteil.

Ohne LINQ erfolgt der Zugriff auf XML-Daten zumeist mit den Klassen XmlDocument, XmlElement, XmlAttribute aus den Namespace System.Xml. Zum direkten Zugriff eignet sich XPath und zum Lesen und Schreiben der Dateien verwendet man häufig XmlReader- und XmlWriter-Objekte.

Mit LINQ ändern sich die verwendeten Objekte. Nun stehen die Klassen XDocument und XElement aus dem Namespace System.Xml.Linq zur Verfügung.

Grundlage für die Beispiele soll eine XML-Datei mit Adressdaten (aus der AdventureWorks-Beispieldatenbank) sein. Diese hat folgenden Aufbau:

<?xml version="1.0" encoding="utf-8"?>
<Adressen>
   <Adresse>
       <Id>1</Id>
       <Name>Guy Gilbert</Name>
       <Address>7726 Driftwood Drive</Address>
       <PostalCode>98272</PostalCode>
       <City>Monroe</City>
       <Country>United States</Country>
       <Phone>320-555-0195</Phone>
       <EmailAddress>guy1@adventure-works.com</EmailAddress>
   </Adresse>
   <Adresse>
       <Id>2</Id>
       <Name>Kevin Brown</Name>
       <Address>7883 Missing Canyon Court</Address>
       <PostalCode>98201</PostalCode>
       <City>Everett</City>
       <Country>United States</Country>
       <Phone>150-555-0189</Phone>
       <EmailAddress>kevin0@adventure-works.com</EmailAddress>
   </Adresse>
</Adressen>

Schreiben von XML-Daten

Schreiben von XML-Daten im .NET 2.0-Style

Zunächst soll die Beispieldatei ohne Verwendung von LINQ erzeugt werden. Damit der Code etwas übersichtlicher wird, hab ich die Erzeugung eines einfachen Elementknotens mit Text in eine separate Funktion ausgelagert.

 

Die eigentliche Routine sieht dann wie im folgenden Beispiel aus:

 

Zunächst wird ein neues XmlDocument erzeugt. Diesem wird zunächst die Xml-Deklaration hinzugefügt. Im Anschluss daran wird der Hauptknoten erzeugt und diesem die beiden Unterknoten hinzugefügt. Das letzte Statement speichert das Dokument.

Schreiben von XML-Dateien mit LINQ

Das Ganze funktioniert auch mit LINQ und - wie ich finde - übersichtlicher und einfacher.

 

Im Konstruktor der Klasse XElement können beliebig viele andere Objekte angegeben werden, so dass schon hier die komplette Dokumentenstruktur aufgebaut werden kann. Mit entsprechenden Einrückungen im Quellcode bleibt es trotzdem übersichtlich. 

Lesen von XML-Daten 

Lesen von XML-Daten im .NET 2.0-Style

Aus der Datei sollen nun die Namen der Personen nach Postleitzahl sortiert ausgegeben werden - der Einfachheit halber auf der Konsole. Auch hierzu zunächst der Quellcode aus .NET 2.0.

 

Zunächst wird die XML-Datei als XmlDocument geladen. Über GetElementsByTagName können alle Elemente mit dem Tag-Namen "PostalCode" ermittelt werden. Diese werden dann in eine generische Liste hinzugefügt, um einfacher mit Hilfe von einer anonymen Methode sortieren zu können.

Das Schema vorausgesetzt kann dann mittels Zugriff auf den ParentNode der Name der zugehörigen Person ausgelesen und ausgegeben werden.

Lesen von XML-Dateien mit LINQ

Auch hier geht alles etwas eleganter und lesbarer mit LINQ.

 

Es wird ein neues XDocument erzeugt, das mittels der statischen Methode Load auch gleich mit Daten befüllt wird. Dann übernimmt eine einzige Linq-Anweisung die komplette Erfüllung der Anforderung. Sie ermittelt alle Namen aus den Adress-Elementen und sortiert diese nach Postleitzahl.

Ähnlich wie LINQ to Objects ist auch LINQ to XML recht einfach in der Handhabung und bringt schnell die gewünschten Ergebnisse.

TraceListener in eine TextBox

In letzter Zeit hab ich ein wenig mit TraceListeners rumprobiert. Die sind sehr praktisch, wenn man schnell ein Log erzeugen will und nicht die Konfigurationsmöglichkeiten von log4net oder ähnlichen Frameworks benötigt.

Als konkreten Anwendungsfall hab ich einen Webpart, den ich loggen wollte, aber auf dem Zielsystem sind nicht ausreichende Berechtigungen, um ins Dateisystem zu schreiben.

Leider bietet mir der TextWriterTraceListener nicht die Möglichkeit, das Ergebnis statt in ein TextFile in eine TextBox zu schreiben. Aus diesem Grund habe ich ich mich dazu entschieden, einen eigenen Listener zu schreiben.

using System;
using System.Diagnostics;
using System.Web.UI.WebControls;

namespace UniqueSoftware.Logging
{
  internal class TextBoxTraceListener : TraceListener
  {
      private TextBox _textbox;

      public override void Write(string message)
      {
          _textbox.Text += message;
      }

      public override void Write(object o)
      {
          _textbox.Text += o.ToString();
      }

      public override void WriteLine(string message)
      {
          _textbox.Text += message + Environment.NewLine;
      }

      public TextBoxTraceListener(TextBox textBox)
      {
          if (textBox == null)
          {
              throw new ArgumentNullException("textBox");
          }
          this._textbox = textBox;
      }
  }
}

Da ich mich nicht in Windows.Forms befinde, muss praktischerweise das Befüllen der TextBox auch nicht mit einem Delegate durchgeführt werden. Das ist auch schon alles. Der TraceListener kann nun genau wie jeder andere Listener verwendet werden.

Die Konfiguration nehm ich programmatisch vor. Damit hat der Anwender eine grafische Benutzeroberfläche zur Anpassung des Loglevels und muss nicht in der web.config Anpassungen vornehmen.

Zunächst werden die Member-Variablen deklariert.

private TextBox logMessages;
private SourceLevels logLevel = SourceLevels.All;
private TraceSource log = null;

Im Konstruktor wird das Log dann initialisiert.

public MyWebPart()
{  
  logMessages = new TextBox();
  log = new TraceSource("MyWebPart");
  log.Switch.Level = logLevel;
  log.Listeners.Add(new Logging.TextBoxTraceListener(logMessages));
}

Nun hab ich noch 3 Methoden, die das Logging durchführen.

#region logging
private void TraceError(string message)
{
  if (log != null)
  {
      message = string.Concat(DateTime.Now.ToString("HH:mm:ss.ff"), " : ", message);
      log.TraceEvent(TraceEventType.Error, 0, message);
  }
}

private void TraceInformation(string message)
{
  if (log != null)
  {
      message = string.Concat(DateTime.Now.ToString("HH:mm:ss.ff"), " : ", message);
      log.TraceEvent(TraceEventType.Information, 0, message);
  }
}

private void TraceWarning(string message)
{
  if (log != null)
  {
      message = string.Concat(DateTime.Now.ToString("HH:mm:ss.ff"), " : ", message);
      log.TraceEvent(TraceEventType.Warning, 0, message);
  }
}
#endregion

Diese Methoden werden je nach Anwendungsfall aufgerufen und ob das Logging durchgeführt wird oder nicht, übernimmt wie gewohnt die TraceSource je nach eingestelltem LogLevel, der nun nur noch per Eigenschaft einstellbar gemacht werden muss.

Thumbnails erzeugen

Wer kennt sie nicht, die kleinen Vorschaubildchen, die man sich ansehen kann, um zu entscheiden, ob man das "richtige" Bild auch ansehen möchte? Heute stand ich vor der Aufgabe, genau diese Bildchen auch zu erstellen.

Nichts einfacher als das, dachte ich mir, denn bei anderen Projekten war ich auch schon über den Namespace System.Drawing gestolpert, der in dieser Beziehung sehr mächtig ist. Sehr überrascht stellte ich fest, dass hier die Entwickler des Framework wohl mitgedacht haben, denn Objekte des Typs Image haben von Haus aus eine Methode GetThumbnailImage, die genau das leisten soll.

Ich schaute mir also die MSDN-Seite zum Thema an und war doch etwas überrascht....

a) Die Methode hat den Parameter callback, der zwar nicht verwendet wird, aber trotzdem erzeugt und übergeben werden muss "In GDI+ ... wird der Delegat nicht verwendet. Sie müssen trotzdem einen Delegaten erstellen und einen Verweis auf diesen in diesem Parameter übergeben."

b) Die Methode hat den Parameter callbackData, der immer mit IntPtr.Zero übergeben werden muss.

"Interessante Methode!" sag ich dazu nur.

Noch überraschter war ich, folgendes Statement zu finden: "GetThumbnailImage funktioniert gut, wenn die angeforderte Miniaturansicht eine Größe von ca. 120 x 120 hat. Eine Anforderung einer großen Miniaturansicht (z. B. 300 x 300) eines Image-Objekts mit einer eingebetteten Miniaturansicht kann zu einem deutlichen Qualitätsverlust bei der Miniaturansicht führen. Es kann ggf. ratsam sein, das Hauptbild anstelle der eingebetteten Miniaturansicht zu skalieren, indem Sie DrawImage aufrufen."

Da ich nicht mit Sicherheit sagen kann, wie groß die Bilder irgendwann mal sein müssen und ich doch eine etwas generischere Methode brauche, entschließe ich mich also, das Bild wie genannt zu skalieren:

private Image GetThumbNailImage(Image BigImage, int ThumbNailWidth, int ThumbNailHeight)
{
// check params
if (BigImage == null)
{
  throw new ArgumentNullException("BigImage");
}

if (ThumbNailHeight <= 0)
{
  throw new ArgumentException("ThumbNailHeight");
}

if (ThumbNailWidth <= 0)
{
  throw new ArgumentException("ThumbNailWidth");
}

// create a new image with new size
Image thumbnail = new Bitmap(ThumbNailWidth, ThumbNailHeight);
// get a handle to the drawing interface for the new image 
Graphics thumbgraphics = Graphics.FromImage(thumbnail);
// draw the big image on the smaller image
thumbgraphics.DrawImage(BigImage, 0, 0, ThumbNailWidth, ThumbNailHeight);

return thumbnail;
}

Soweit, so gut... Nun hat man aber nicht immer die neuen Größen bei der Hand, sondern möchte auch mal nur die Breite oder die Höhe angeben und das Programm soll selbst den jeweiligen anderen Parameter errechnen und das Seitenverhältnis beibehalten. Also schreib ich mir noch schnell 2 Methoden, die diese Funktionalität bieten.

private Image GetThumbNailImageFixedWidth(Image BigImage, int ThumbNailWidth)
{
// check params
if (BigImage == null)
{
   throw new ArgumentNullException("BigImage");
}
if (ThumbNailWidth <= 0)
{
  throw new ArgumentException("ThumbNailWidth");
}

double ratio = (double) BigImage.Width / (double) BigImage.Height;

int newwidth = ThumbNailWidth;
int newheight = (int)(newwidth / ratio);

return GetThumbNailImage(BigImage, newwidth, newheight);
}
private Image GetThumbNailImageFixedHeight(Image BigImage, int ThumbNailHeight)
{
// check params
if (BigImage == null)
{
  throw new ArgumentNullException("BigImage");
}
if (ThumbNailHeight <= 0)
{
  throw new ArgumentException("ThumbNailHeight");
}

double ratio = (double)BigImage.Width / (double)BigImage.Height;

int newheight = ThumbNailHeight;
int newwidth = (int)(newheight * ratio);

return GetThumbNailImage(BigImage, newwidth, newheight);
}

Das war's auch schon. Für den Test nutz ich dann mal die Beispielbilder....

// Originalbild
Image bigimage = Image
.FromFile("C:\\Users\\Public\\Pictures\\Sample Pictures\\Desert Landscape.jpg"); // neues Bild mit vorgegebener Höhe Image smallimagebyfixedheight = GetThumbNailImageFixedHeight(bigimage, 200); // neues Bild mit vorgegebener Breite Image smallimagebyfixedwidth = GetThumbNailImageFixedWidth(bigimage, 200); // Thumbnails speichern smallimagebyfixedheight.Save("C:\\testimageh.jpg", ImageFormat.Jpeg); smallimagebyfixedwidth.Save("C:\\testimagew.jpg", ImageFormat.Jpeg);

That's it !

Das COM-.NET-PInvoke-Experiment

In den letzten Tagen hab ich mich intensiv mit COM-Interop in .NET beschäftigt. Ich muss sagen: Das Ganze war nicht ganz so einfach wie ich es mir vorgestellt hatte, aber wenn man weiß wie es geht, ist es gar nicht so schwer.

Was will ich machen: Ich brauche eine .NET-Komponente, die mittel PInvoke die Twain-Schnittstelle eines Scanners anspricht. .NET in diesem Fall, weil das System, das die Komponente verwendet in naher Zukunft auch auf .NET umgestellt werden sein wird und ich dann nicht nochmal mit einer Umstellung beginnen möchte. Leider sind einige Teile des Frontend noch auf VB6-Basis und deshalb muss meine Middleware-Komponente auch COM-Interop unterstützen.

PInvoke an sich scheint kein größeres Problem darzustellen - sollte es doch eines sein, dann werd ich zu diesem Zeitpunkt hier nochmal näher auf dieses Thema eingehen. Was mich deshalb mehr interessiert, war der Punkt mit COM-Interoperabilität.

Als verwöhnter Visual-Studio-Microsoft-Programmierer hab ich es mir ganz einfach vorgestellt: Ich erstelle eine Klassenbibliothek, setze in den Projekteigenschaften den Haken "für COM-Interop registrieren" und setze bei den entsprechenden Klassen, Methoden und Events das Attribut ComVisible auf true. Aber weit gefehlt.

Ich hab also meinen gerade genannten ersten Ansatz wieder verworfen und mich auf die Suche nach den perfekten funktionierenden Einstellungen im Internet gemacht.

Meinen Erkenntnissen nach einigen Stunden Foren-, Newsgroup- und MSDN-Suche sind folgende Einstellungen wichtig:

Zunächst ein Eintrag in der AssemblyInfo.cs - hier bin ich mir nicht sicher, ob der wirklich notwendig ist, aber es funktioniert so und deshalb lass ich die Einträge erstmal drin. Hier wird definiert, dass das Assembly an sich sichtbar für COM ist und mit welcher Guid.

[assembly: ComVisible(true)]

[assembly: Guid("a3b03ad9-34bf-4357-a41f-be532b904c81")]
[assembly: ClassInterface(ClassInterfaceType.AutoDual)]

Meine zu veröffentlichende Klasse selbst besitzt einige öffentliche Methoden und Eigenschaften sowie Events. Nun wird es interessant. Die Klasse selbst wird nicht für COM sichtbar gemacht, sondern nur ein Interface, das die Schnittstelle beschreibt. In meinem Fall sieht das so aus:

[Guid("F8E22BD6-1C73-467B-B6D2-DF33DFD6861F"), 
InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface ITwain
{
    [DispId(0x8001)]
    bool Scanning { get; }

    [DispId(0x8002)]
    int ImageCount { get; }

    [DispId(0x8101)]
    void SelectScanner();

    [DispId(0x8102)]
    void AcquireImage();

    [DispId(0x8103)]
    bool SaveImage(string location, ImageFormats format)
}

Das Interface erhält für COM das Guid-Attribut und das InterfaceType-Attribut. Jeder Methode und Eigenschaft sollte noch eine DispId zugewiesen werden. Einigen Foreneinträgen zufolge kommt es bei der automatischen Zuweisung bei der Kompilierung sonst zu seltsamen Nebeneffekten. In welchem Nummernbereich diese DispId's liegen sollten, ist mir noch nicht ganz klar. Man findet Beispiele, wo mit 1 angefangen und dann durchnummeriert wird und es finden sich ebenso auch Beispiele, in denen Nummernbereiche definiert werden wie ich sie verwendet hab.

Noch interessanter wird es bei den Events. Wichtig ist, dass die Delegates für COM unsichtbar sind. Weiterhin ist nicht der Event selbst für COM zu veröffentlichen. Das war mein erster Ansatz und er führte dazu, dass das "syntaktische Zuckerstück" Event in seine Basismethoden aufgeschlüsselt wird und diese 3 Methoden im COM sichtbar sind. Da das nicht das gewünschte Ergebnis ist, ist hier anders vorzugehen.

Auch für die Events wird wieder ein Interface definiert. Auch dieses Interface erhält für COM das Guid-Attribut und das InterfaceType-Attribut und für jeden Event wird eine Methode definiert, der eine DispId zugewiesen wird.

[Guid("AEF23281-AAC1-401D-B798-EC4B999CD85E"), 
InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface ITwainComEvents
{
    [DispId(0x8201)]
    void ScanComplete();

    [DispId(0x8202)]
    void ScanStarted();

    [DispId(0x8203)]
    void ScanAborted();

    [DispId(0x8204)]
    void ScanImageCompleted();
}

So weit so gut. Nun muss der eigentlichen implementierenden Klasse noch mitgeteilt werden, dass die Interfaces wichtig sind. Das Methoden-Property-Interface wird wie gewohnt angegeben. Das Event-Interface wird als ComSourceInterface verwendet. Wichtig ist hier noch, dass die eigentliche Klasse ihre Schnittstelle nicht exportieren darf (ClassInterfaceType.None).

[Guid("15BDE204-511E-411E-9A52-F117A8C8BAD7")]
[ClassInterface(ClassInterfaceType.None)]
[ComSourceInterfaces(typeof(ITwainComEvents))]
[SecurityPermission(SecurityAction.Demand, UnmanagedCode = true)]
public class Twain : ITwain, IDisposable

Und das war's auch schon. Nun wird die Assembly ganz normal kompiliert.

Wie verwendet man die Klasse nun aber im COM?

Damit das möglich ist, muss die TypeLib noch bereitgestellt werden. Hierfür kann man das Programm TlbExp verwenden. Damit erhält man eine tlb-Datei. Diese kann man dann mit RegAsm registrieren.

Wichtig in den "Verweisen" der COM-konsumierenden-Komponente ist nun, dass man auf die TypeLib verweist und nicht wie gewohnt auf die dll.