head.WriteLine()

Donnerstag, Oktober 26, 2006

ConsoleTraceListener in WinForms nutzen, Teil 3

Wie im ersten und zweiten Teil bereits gezeigt, ist die Anzeige eines Konsolenfensters eine recht praktische Sache. Doch es geht noch schöner: Wenn Sie in Ihrer Anwendung mit Docking Windows arbeiten oder ein spezielles Trace-Panel in Ihrem Hauptfenster haben, können Sie den Trace-Output der Konsole auf dieses umleiten. Und das Beste: Sie kommen hierbei sogar völlig ohne Windows-API-Deklarationen aus!

Für die Umleitung der Anwendungskonsole bietet die Console-Klasse die Methode SetOut(). Diese nimmt eine Instanz von System.IO.TextWriter entgegen und leitet in diese fortan die Ausgaben weiter. Somit könnten Sie den Output sehr leicht in eine Textdatei umleiten, doch dann hätten Sie kein Echtzeit-Tracing in der UI.

Daher müssen Sie eine Ableitung von TextWriter erstellen und die Daten beispielsweise an eine Textbox übergeben. Der Einfachheit halber habe ich mir dafür eine spezielle TextBox-Ableitung namens StandardOutputTextBox erstellt:

using System;
using System.ComponentModel;
using System.Windows.Forms;

[System.ComponentModel.DesignerCategory("Code")]
public class StandardOutputTextBox : TextBox
{
    private TextBoxWriter m_writer = null;

    [Browsable(false)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
    public TextBoxWriter Writer
    {
        get
        {
            if (m_writer != null)
            {
                return m_writer;
            }
            return new TextBoxWriter(this);
        }
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing && (m_writer != null))
        {
            m_writer.Dispose();
        }
        base.Dispose(disposing);
    }
}

Dies bekommt eine Instanz von TextBoxWriter übergeben, meiner TextWriter-Ableitung. Dieser wird wiederum eine Instanz der StandardOutputTextBox übergeben, in die getraced werden soll.

TextBoxWriter sieht hierbei wie folgt aus:

using System;

public class TextBoxWriter : System.IO.TextWriter
{
    private System.Windows.Forms.TextBox m_box = null;

    public TextBoxWriter(System.Windows.Forms.TextBox box)
    {
        m_box = box;
    }

    public System.Windows.Forms.TextBox TextBox
    {
        get { return m_box; }
        set { m_box = value; }
    }

    public override System.Text.Encoding Encoding { get { return System.Text.Encoding.ASCII; } }
    public override void Write(bool value) { this.Write(value.ToString()); }
    public override void Write(char value) { this.Write(value.ToString()); }
    public override void Write(char[] buffer) { this.Write(new string(buffer)); }
    public override void Write(char[] buffer, int index, int count) { this.Write(new string(buffer, index, count)); }
    public override void Write(decimal value) { this.Write(value.ToString()); }
    public override void Write(double value) { this.Write(value.ToString()); }
    public override void Write(float value) { this.Write(value.ToString()); }
    public override void Write(int value) { this.Write(value.ToString()); }
    public override void Write(long value) { this.Write(value.ToString()); }
    public override void Write(string format, object arg0) { this.WriteLine(string.Format(format, arg0)); }
    public override void Write(string format, object arg0, object arg1) { this.WriteLine(string.Format(format, arg0, arg1)); }
    public override void Write(string format, object arg0, object arg1, object arg2) { this.WriteLine(string.Format(format, arg0, arg1, arg2)); }
    public override void Write(string format, params object[] arg) { this.WriteLine(string.Format(format, arg)); }
    public override void Write(uint value) { this.WriteLine(value.ToString()); }
    public override void Write(ulong value) { this.WriteLine(value.ToString()); }
    public override void Write(object value) { this.WriteLine(value.ToString()); }
    public override void WriteLine() { this.WriteLine(Environment.NewLine); }
    public override void WriteLine(bool value) { this.WriteLine(value.ToString()); }
    public override void WriteLine(char value) { this.WriteLine(value.ToString()); }
    public override void WriteLine(char[] buffer) { this.WriteLine(new string(buffer)); }
    public override void WriteLine(char[] buffer, int index, int count) { this.WriteLine(new string(buffer, index, count)); }
    public override void WriteLine(decimal value) { this.WriteLine(value.ToString()); }
    public override void WriteLine(double value) { this.WriteLine(value.ToString()); }
    public override void WriteLine(float value) { this.WriteLine(value.ToString()); }
    public override void WriteLine(int value) { this.WriteLine(value.ToString()); }
    public override void WriteLine(long value) { this.WriteLine(value.ToString()); }
    public override void WriteLine(string format, object arg0) { this.WriteLine(string.Format(format, arg0)); }
    public override void WriteLine(string format, object arg0, object arg1) { this.WriteLine(string.Format(format, arg0, arg1)); }
    public override void WriteLine(string format, object arg0, object arg1, object arg2) { this.WriteLine(string.Format(format,
arg0, arg1, arg2)); }
    public override void WriteLine(string format, params object[] arg) { this.WriteLine(string.Format(format, arg)); }
    public override void WriteLine(uint value) { this.WriteLine(value.ToString()); }
    public override void WriteLine(ulong value) { this.WriteLine(value.ToString()); }
    public override void WriteLine(object value) { this.WriteLine(value.ToString()); }

    public override void Write(string value)
    {
        m_box.Text = m_box.Text + value;
    }

    public override void WriteLine(string value)
    {
        m_box.Text += value + Environment.NewLine;
        System.Windows.Forms.Application.DoEvents();
    }
}

Wie Sie sehen passiert hier nichts Spektakuläres, es wird einfach der hereinkommende Content in die Textbox geschrieben.

Nun können Sie die StandardOutputTextBox auf eine Form ziehen und den Konsolen-Output an deren Writer-Eigenschaft umleiten.

public partial class MainForm : Form
{
    public MainForm()
    {
        InitializeComponent();

        // Console umleiten
        Console.SetOut(this.consoleTextBox.Writer);

        // ConsoleTraceListener anmelden
        Trace.Listeners.Add(new ConsoleTraceListener());
    }

    private void traceButton_Click(object sender, EventArgs e)
    {
        Trace.WriteLine("Hello Console Trace!");
    }
}

Das Ergebnis sieht nun erwartungsgemäß so aus:

ConsoleTraceListener in WinForms nutzen, Teil 2

Wie Sie im ersten Teil bereits sehen konnten, ist die Anzeige eines separaten Trace-Konsolenfensters recht hilfreich bei der Laufzeitanalyse. Das Ändern des Projekttyps war hierbei zwar denkbar einfach, aber nicht unbedingt schön für den Produktivbetrieb beim Kunden. Schöne wäre es doch, wenn man das Konsolenfester dynamisch bei Bedarf starten könnte. Und das geht so:

Zunächst müssen Sie die Windows-API-Funktionen AllocConsole() und FreeConsole() deklarieren:

[DllImport("kernel32.dll")]
public static extern Boolean AllocConsole();

[DllImport("kernel32.dll")]
public static extern Boolean FreeConsole();

Nun können Sie dynamisch ein Konsolenfenster erstellen und ganz normal über den ConsoleTraceListener tracen:

AllocConsole();
Trace.Listeners.Add(new ConsoleTraceListener());
Trace.WriteLine("Hello Console Trace!");


Zum Schließen des Fensters rufen Sie schließlich FreeConsole() auf.

Im dritten und letzten Teil dieser kleinen Serie erfahren Sie, wie Sie den Konsolen-Output in ein WinForms-Control umleiten können.

ConsoleTraceListener in WinForms nutzen, Teil 1

Der ConsoleTraceListener ist neu in .NET 2.0 und ermöglicht, wie der Name schon sagt, die Ausgabe von Trace-Informationen in die Konsole. Das ist besonders für Windows-Forms-Anwendungen interessant, bietet es doch die Möglichkeit, während der Ausführung die Trace-Daten in Echtzeit zu überwachen.

Doch wie bekommt man die Konsolendaten in einer WinForms-Anwendung angezeigt? Hierfür gibt es drei verschiedene Ansätze:
  • Starten einer Konsolenanwendung, die ein WinForms-Fenster öffnet
  • Starten einer WinForms-Anwendung, die ein Konsolenfenster öffnet
  • Umleiten des Konsolen-Outputs in ein WinForms-Control

Die erste Variante ist am einfachsten. Hierbei stellen Sie in den Projekteigenschaften den Ausgabetyp der WinForms-Anwendung auf Konsole-Anwendung. Wenn Sie die Anwendung nun starten, sieht das in etwa wie folgt aus:

Nun können Sie den ConsoleTraceListener über Trace.Listener einhängen und drauf los tracen. Die Ausgabe wird daraufhin im parallel laufenden Konsolenfenster ausgegeben.

Trace.Listeners.Add(new ConsoleTraceListener());
Trace.WriteLine("Hello Console Trace!");

Das Ganze hat jedoch den etwas angestaubten Charme einer Java-Anwendung ;) Wie es eleganter geht, erfahren Sie im zweiten Teil.

Freitag, Oktober 20, 2006

WPF-Artikel in der Computer World Schweiz

In der aktuellen Ausgabe der Computer World Schweiz (41/2006) ist mein Artikel „Webanwendungen jenseits von HTML“ erschienen.

Aus dem Abstract:
Zurzeit müssen sich Entwickler noch immer zwischen verschiedenen Technologien entscheiden, abhängig davon, ob die Anwendung später auf dem Desktop oder im Browser laufen soll. Mit der Windows Presentation Foundation (WPF) versucht Microsoft nun diesen Spagat durch ein einheitliches Programmiermodell zu lösen. Daher zielt WPF sowohl auf Desktop, als auch auf Intra- und Internet-Anwendungen ab. Die internen API’s bzw. das verwendete XAML-Markup ist hierbei jedoch nahezu identisch, sodass der Entwickler im Idealfall den vorhandenen Code lediglich in verschiedenen Versionen bereitstellen muss.

Montag, Oktober 02, 2006

Entwurfszeit-Attribute, Teil 4: Data Binding

War es in .NET 1.x relativ schwer die eigenen Komponenten und Geschäftsobjekte datenbindungsfähig zu machen, so ist dies in .NET 2.0 schon fast ein Kinderspiel.

Objektdatenquellen
Vielleicht wollen Sie Ihre Geschäftsobjekte zur Entwurfszeit an eine Oberfläche binden, wie sie dies von typisierten Datasets kennen. Hierzu erstellen Sie eine Klasse und versehen sie mit entsprechenden Eigenschaften.
Beispiel:

public class Person
{
    private string m_firstName;
    private string m_lastName;

    public Person() {}

    public string FirstName
    {
        get { return m_firstName; }
        set { m_firstName = value; }
    }

    public string LastName
    {
        get { return m_lastName; }
        set { m_lastName = value; }
    }
}

Jetzt bietet Ihnen Visual Studio 2005 über das neue Data Source Window die Möglichkeit, basierend auf einer Datenquelle eine entsprechende Oberfläche zu erstellen und diese zu binden. Hierzu erstellen Sie mit dem zugehörigen Wizard zunächst eine neue Datenquelle. Hierbei geben Sie als Datenquellentyp "Object Source" an und wählen die Person-Klasse aus. Wenn Sie nun den entsprechenden Eintrag aus dem Data Source Window auf eine Form ziehen, werden - je nach Einstellung - entweder ein Grid oder entsprechende Formularfelder eingefügt. Hierbei werden die Spaltentitel anhand der Eigenschaftennamen gewählt. Sollen hierbei jedoch nicht die meist englischen Eigenschaftennamen, sondern beispielsweise die entsprechenden deutschen Bezeichner verwendet werden, so können Sie dies über die Deklaration des DisplayName-Attributs in Ihrer Datenklasse steuern. Beispiel:

[DisplayName("Vorname")]
public string FirstName
{
    get { return m_firstName; }
    set { m_firstName = value; }
}

[DisplayName("Nachname")]
public string LastName
{
    get { return m_lastName; }
    set { m_lastName = value; }
}

Steuerelemente bindungsfähig machen
Um nun Ihre eigenen Steuerelemente bindungsfähig zu machen, ist im Idealfall keine Zusatzarbeit notwendig. Der Grund hierfür ist, dass die Control-Klasse das IBindingComponent-Interface implementiert, über das die entsprechenden Bindungsinformationen bereitgestellt werden. Wenn Sie nun direkt oder indirekt von Control ableiten und lediglich einen einzelnen Wert anzeigen, ist keine Zusatzarbeit notwenig.

Anders sieht es jedoch aus, wenn Ihr Steuerelement mehrere Werte oder Datenzeilen anzeigen soll. In diesem Fall müssen Sie entsprechende Eigenschaften für die Datenquelle bereitstellen.
  • DataSource
  • DataMember
  • DisplayMember
  • ValueMember
Die letzten beiden benötigen Sie jedoch nur, wenn Sie beispielsweise eine ComboBox oder ähnliches implementieren und hierbei auf zwei Datenquellen zugreifen wollen.

Um nun ein ähnliches Entwurfszeitverhalten für die Eigenschaften zu bieten, wie Sie dies beispielsweise vom DataGridView kennen, sollten Sie die folgenden Attribute notieren:

[RefreshProperties(RefreshProperties.Repaint)]
[AttributeProvider(typeof(IListSource))]
[DefaultValue(null)]
public object DataSource
{

}

[Editor("System.Windows.Forms.Design.DataMemberListEditor, System.Design, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))]
public string DataMember
{

}

Über die Bedeutung dieser Attribute finden Sie in einem älteren Blog-Eintrag mehr.

Unterstützung für das Data Source Window implementieren
Jetzt wäre es ja schön, wenn auch Ihr Steuerelement als Option im Data Source Window erscheinen und per Drag & Drop auf eine Form gebracht und gebunden werden könnte. Hierfür dekorieren Sie Ihre Steuerelementklasse - je nach Typ - mit den folgenden Attributen:
  • DefaultBindingProperty
  • ComplexBindingProperties
  • LookupBindingProperties
DefaultBindingProperty verwenden Sie, wenn Ihr Steuerelement lediglich einen einzelnen Wert abbildet. Hierbei verweist es auf die zu bindendene Eigenschaft. Beispiel:

[DefaultBindingProperty("Text")]

ComplexBindingProperties ist hingegen für die komplexe Bindung, sprich mehrere Werte bzw. Datenzeilen. Es verweist auf die Eigenschaften DataSource und DataMember.

[ComplexBindingProperties("DataSource", "DataMember")]

Bei ComboBox und co. verweist LookupBindingProperties auf die entsprechenden Eigenschaften der Wert- bzw. Lookup-Datenquelle.

[LookupBindingProperties(
"DataSource", "DisplayMember", "ValueMember", "LookupMember")]

Entwurfszeit-Attribute, Teil 3: Code-Generierung

Wie Sie im ersten Teil bereits gesehen haben, können Sie bestimmte Eigenschaften mit Hilfe des Browsable-Attributs zur Entwurfszeit ausblenden. Da der Entwickler hierbei die Eigenschaft nicht ändern kann, ist es meist auch nicht notwendig für diese Code zu generieren. Standardmässig wird ja für jede Eigenschaft entsprechender Code in der InitializeComponent()-Methode der Form angelegt, in der die Komponente eingesetzt wird. Wenn Sie die Eigenschaft jedoch mit dem DesignerSerializationVisibility-Attribut versehen und dieses auf den Wert DesignerSerializationVisibility.Hidden setzen, wird die Code-Generierung vollständig unterdrückt.

[Browsable(false),
DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Hidden)]
public string MyProperty
{
...
}

Wenn Ihre Eigenschaft lediglich zur Entwurfszeit verwendet wird, können Sie Ihre Eigenschaft mit dem DesignOnly-Attribut versehen und dieses auf true setzen. Wie auch beim DesignerSerializationVisibility-Attribut, wird der Wert nicht in Code-Form serialisiert, jedoch in der zugehörigen Resource-Datei gespeichert. Hierdurch können Sie das Editieren zur Entwurfszeit ermöglichen, ohne das hierfür Laufzeit-Code erstellt werden muss.

Standardwerte
Über das DefaultValue-Attribut können Sie einen Standardwert für die Eigenschaft festlegen. Dies ist immer dann sinnvoll, wenn die Eigenschaft einen einfachen Datentyp wie int, string, usw. repräsentiert.

[DefaultValue(false)]
public bool AllowEdit
{
..
}

Wenn der Entwickler diese Eigenschaft nicht verändert, so wird auch kein Code erzeugt, da der angegebene Wert dem Standard entspricht. Darüber hinaus ergibt sich bei Änderungen ein schöner Effekt: Das PropertyGrid zeigt nämlich die Werte aller geänderten Eigenschaften in fetter Schrift an. So fällt es dem Entwickler sehr leicht die Eigenschaften auszumachen, die von ihm verändert wurden. Wäre das DefaultValue-Attribut hingegen nicht gesetzt gewesen, würde immer Code generiert und auch der Standardwert im PropertyGrid fett dargestellt.

Einfache Enumeration-Werte und Strukturen können übrigens auch über das DefaultValue-Attribut abgebildet werden, wie das folgende Beispiel zeigt:

[DefaultValue(typeof(Color), "Black")]
public Color BackColor
{
..
}

Serialisierung komplexer Typen steuern
Nun gibt es jedoch auch Fälle, in denen die Eigenschaft einen komplexen Typen abbildet. Hier können Sie nicht das DefaultValue-Attribut verwenden. Stattdessen müssen Sie zwei Methoden implementieren, die dem folgenden Muster entsprechen:

ShouldSerializeEigenschaftenName()
ResetEigenschaftenName()

Der Windows Forms Code-Serializer prüft hierbei für jede Eigenschaft, ob entsprechende Methoden vorhanden sind. Ist dies der Fall, ruft er ShouldSerializeXXX auf, um zu ermitteln, ob für diese Code generiert weden soll. Wird zusätzlich auch ResetXXX angeboten, bietet das Eigenschaftenfenster über sein Kontextmenü die Möglichkeit, die Eigenschaft auf ihren Standardwert zurückzusetzen. Das folgende Beispiel zeigt die Implementierung:

private MyComplexType m_myComplexProperty

public MyComplexType MyComplexProperty
{
    get { return m_myComplexProperty ; }
    set { m_myComplexProperty = value; }
}

private bool ShouldSerializeMyComplexProperty()
{
    return (m_myComplexProperty != null);
}

private void ResetMyComplexProperty()
{
    m_myComplexProperty = null;
}

Hierbei können die Methoden ruhig als private implementiert werden, da der Serializer per Reflection auf diese zugreift.

Komplexe Typen serialisieren
Besteht eine Eigenschaft aus einem Unterobjekt, das der Komponente nicht zugewiesen wurde, sondern von dieser selbst verwaltet wird, müssen Sie die Eigenschaft mit dem DesignerSerializationVisibility-Attribut auszustatten und dieses auf DesignerSerializationVisibility.Content setzen. Außerdem ist für die Unterklasse ein TypeConverter zu implementieren. In diesem kann entweder CreateInstance() oder ConvertTo() überschrieben werden, um die Codegenerierung zu unterstützen.
Das folgende Beispiel zeigt, wie Sie Deklaration und Implementierung vornehmen. Die Komponente MyComponent enthält die Person-Eigenschaft des gleichnamigen Typs. Dieser sieht wie folgt aus:

public class Person
{
    private string m_firstName;
    private string m_lastName;

    public Person()
    {
        m_firstName = string.Empty;
        m_lastName = string.Empty;
    }

    public Person(string firstName, string lastName)
    {
        m_firstName = firstName;
        m_lastName = lastName;
    }

    public string FirstName
    {
        get { return m_firstName; }
        set { m_firstName = value; }
    }

    public string LastName
    {
        get { return m_lastName; }
        set { m_lastName = value; }
    }

    public override string ToString()
    
{
        if (m_lastName != null && m_firstName != null &&
m_lastName.Length > 0 && m_firstName.Length > 0)
        {
            return m_lastName + ", " + m_firstName;
        }
        else
        {
            return null;
        }
    }
}

Interessant ist hierbei eigentlich nur die überschriebene ToString()-Methode. Sie ist für die spätere Darstellung im Eigenschaftenfenster zuständig, doch dazu später mehr.
Schauen Sie sich nun die Deklaration der Person-Eigenschaft an:

private Person m_person;

[TypeConverter(typeof(PersonConverter))]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public Person Person
{
    get { return m_person; }
    set { m_person = value; }
}

Wie Sie sehen, ist die Eigenschaft mit dem DesignerSerializationVisibility-Attribut ausgestattet und dessen Wert auf Content gesetzt worden. Dies ist wichtig, um dem Serializer mitzuteilen, dass die Eigenschaft mit Hilfe des ebenfalls deklarierten TypeConverters serialisiert werden soll.

Mit Hilfe des TypeConverter-Attributs verweisen Sie nun auf den entsprechenden PersonConverter, der für die Konvertierung und Serialisierung von Person-Instanzen zuständig ist.

Zustätzlich sollten Sie daran denken, die private Variable m_person im Konstruktor zu initialisieren, da das PropertyGrid die Person-Eigenschaft sonst nicht anzeigen kann.

public MyComponent()
{
    m_person = new Person();
}

Kommen wir nun zur Implementierung der PersonConverter-Klasse.

internal class PersonConverter : TypeConverter
{
...
}

Diese leitet von TypeConverter ab und überschreibt die folgenden Methoden:
  • CanConvertFrom()
  • ConvertFrom()
  • CanConvertTo()
  • ConvertTo()

Erstere werden vom PropertyGrid aufgerufen, nachdem der Entwickler der Person-Eigenschaft einen Wert zugewiesen hat.

Da das PropertyGrid den Person-Typ nicht kennt, ruft es nun die ConvertFrom()-Methode von PersonConverter auf, die den Text analysiert und ein entsprechendes Person-Objekt zurückgibt.

public override bool CanConvertFrom(
    ITypeDescriptorContext context,
    Type sourceType)
{
    
if (sourceType == typeof(System.String))
        return true;
    return base.CanConvertFrom(context, sourceType);
}

public override object ConvertFrom(
    ITypeDescriptorContext context,
    System.Globalization.CultureInfo culture,
    object value)
{
    if (value is System.String)
    {
        string name = value.ToString();
        Person p = new Person();

        if (name.Length == 0)
        {
            p.FirstName = string.Empty;
            p.LastName = string.Empty;
            return null;
    }

    int pos = name.IndexOf(",");
    if (pos > 0)
    {
        p.LastName = name.Substring(0, pos -1).Trim();
        p.FirstName = name.Substring(pos + 1).Trim();
    }
    else
    {
        p.FirstName = string.Empty;
        p.LastName = name.Trim();
    }
    return p;
  }
  return base.ConvertFrom(context, culture, value);
}

Damit das PropertyGrid den für ihn unbekannten Typ Person darzustellen, benötigt es die CanConvertTo()- und die ConvertTo()-Methode. Während erstere darüber informiert, ob das angegebene Objekt gewandelt werden kann, gibt letztere einen sogenannten InstanceDescriptor zurück. Dieser ist für die spätere Serialisierung von Interesse, da er ein ConstructorInfo-Objekt beinhaltet, der darüber Auskunft gibt, welcher Konstruktor zum Füllen der Instanz aufgerufen werden soll.

public override bool CanConvertTo(ITypeDescriptorContext context, Type destType)
{
    if (destType == typeof(InstanceDescriptor))
        return true;
    return base.CanConvertTo(context, destType);
}

public override object ConvertTo(
    ITypeDescriptorContext context,
    System.Globalization.CultureInfo culture,
    object value,
    Type destType)
{
    if (destType == typeof(InstanceDescriptor))
    {
        Person p = value as Person;

        // Konstruktor ermitteln
        ConstructorInfo ci = typeof(Person).GetConstructor(
        new Type[]{ typeof(string), typeof(string) } );

        // Neue Instanz erstellen
        return new InstanceDescriptor(ci,
        new object[] { p.FirstName, p.LastName }, true);
    }
    return base.ConvertTo(context, culture, value, destType);
}

Eigenen Serializer implementieren
Möchte man noch tiefer in die Codegenerierung eingreifen (z.B. um der Komponente bereits im Konstruktor alle Eigenschaften zu übergeben) so ist die Komponente mit dem DesignerSerializer-Attribut auszustatten, welches auf den eigenen Serializer-Typ verweist. Dieser leitet von CodeDomSerializer ab und überschreibt die Methoden Serialize() und Deserialize(). Nun können Sie entweder das komplette CodeDom-Konstrukt selbst erzeugen oder zunächst einen Standardserializer mit der Codegenerierung beauftragen und das erzeugte CodeDom-Konstrukt modifizieren. Der so erzeugte Code wird später in die InitializeComponent()-Methode eingefügt.

Um Code zu erzeugen, der außerhalb der InitializeComponent()-Methode liegt, muss ein sogenannter RootDesignerSerializer erstellt werden, der einer Klasse zugewiesen wird, die über einen eigenen RootDesigner verfügt, wie Form oder Component. Der RootDesignerSerializer wird ebenfalls über das DesignerSerializer-Attribut zugewiesen. In beiden Serializer-Typen kann jedoch nicht die volle Bandbreite der von CodeDom gebotenen Möglichkeiten genutzt werden. So ist beispielsweise das Anlegen von Eigenschaften oder die Definition von Methodenrückgaben nicht möglich.

Entwurfszeit-Attribute, Teil 2: Interaktion mit dem PropertyGrid

Standardeigenschaft und Event einer Komponente bestimmen
Wenn Sie eine Komponente im Designer auswählen und dann in das Eigenschaftenfenster wechseln, wird die entsprechende Standardeigenschaft ausgewählt. Welche dies sein soll, können Sie auf über das DefaultProperty-Attribut festlegen, dass auf Klassenebene deklariert wird.

[DefaultProperty("MyProperty")]
public class MyComponent : Component
{
...
}

Das gleiche können Sie auch für Events bewirken. Hierfür deklarieren Sie das DefaultEvent-Attribut.

[DefaultEvent("MyEvent")]
public class MyComponent : Component
{
...
}


Eigenschaft mit Schreibschutz versehen
Möchten Sie hingegen, dass eine Eigenschaft zur Entwurfszeit sichtbar ist, vom Entwickler jedoch nicht geändert werden darf, so können Sie diese mit dem ReadOnly-Attribut ausstatten.

[ReadOnly(true)]
public string MyProperty
{
...
}

Dieser Schreibschutz bezieht sich jedoch nur auf das Eigenschaftenfenster, eine programmatische Änderung ist weiterhin möglich.

Den Eigenschaftennamen in Klammern setzen
Einige Eigenschaften, wie beispielsweise die Dynamic Properties der Form-Klasse, werden im Eigenschaftenfenster in Klammern darstestellt. Wenn Sie diese Darstellung für eine Ihrer Eigenschaften realisieren wollen, deklarieren Sie das ParenthesizePropertyName-Attribut und setzen es auf true.

[ParenthesizePropertyName(true)]
public string MyProperty
{
...
}

Beschreibungen und Kategorien
Um dem Entwickler die Eigenschaft im PropertyGrid näher zu erläutern, können Sie dieses mit dem Description-Attribut versehen und mit einem entsprechenden Beschreibungstext füllen. Dieser wird dann im unteren Bereich des PropertyGrids angezeigt.

Das Eigenschaftenfenster zeigt standardmässig die Eigenschaften in einer kategorisierten Ansicht an. Unter welcher Kategorie Ihre Eigenschaft angezeigt werden soll, können Sie über das Category-Attribute bestimmen. Beispiel:

[Category("Data")]
public string MyProperty
{
...
}

Die möglichen Werte sind hierbei:

  • Action
  • Appearance
  • Behavior
  • Data
  • Default
  • Design
  • DragDrop
  • Focus
  • Format
  • Key
  • Layout
  • Mouse
  • WindowStyle

Eigenschaften mergen
Ist Ihnen schon einmal aufgefallen, dass Sie bestimmte Eigenschaften für mehrere Komponenten gleichzeitig zuweisen können? Wenn Sie beispielsweise zwei Textboxen auswählen, können Sie im Eigenschaftenfenster für beide die Visible-Eigenschaft setzen. Bei anderen Eigenschaften, wie Text ist dies hingegen nicht möglich. Die Steuerung dieses Verhaltens findet hierbei über das MergableProperty-Attribut statt. Steht dieses auf true, so kann die entsprechende Eigenschaft auch für mehr als eine Komponenteninstanz zugewiesen werden.

[MergableProperty(true)]
public string MyProperty
{
...
}


Aktualisierungen
Bei Controls ist es oft sinnvoll, die Entwurfsoberfläche nach dem Ändern einer Eigenschaft zu aktualisieren. Dies können Sie entweder manuell in der Eigenschaft bewerkstelligen, oder automatisiert durch das Setzen des RefreshProperties-Attributs. Dieses wird vom Designer ausgewertet und ggf. eine Aktualisierung vorgenommen. In welcher Form dies geschehen soll, bestimmen Sie über einen Wert der RefreshProperties-Enumeration. Hierbei können Sie zwischen den folgenden Werten wählen:

  • All
  • Default
  • Repaint

Um beispielsweise ein Neuzeichnen nach der Änderung zu bewirken, könnten Sie das Attribut wie folgt deklarieren:

[RefreshProperties(RefreshProperties.Repaint)]
public string MyProperty
{
...
}

Entwurfszeit-Attribute, Teil 1: Interaktion mit Visual Studio

Komponenten in der Code-Ansicht öffnen
Für Komponenten, Controls und Forms bietet Visual Studio einen eigenen Designer, der automatisch geöffnet wird, wenn Sie im Solution Explorer einen Doppelklick auf die entsprechende Datei machen. Oftmals wollen Sie jedoch lediglich in die Code-Ansicht. Nun können Sie dies zwar auch über die entsprechende Schaltfläche im Solution Explorer-Windows erreichen, ein Doppelklick ist jedoch um einiges schneller. Glücklicherweise existiert über das DesignerCategory-Attribut die Möglichkeit, das Standardverhalten anzupassen, wie das folgende Beispiel zeigt:

[System.ComponentModel.DesignerCategory("Code")]
public class MyComponent : Component
{
...
}

Hierbei ist es wichtig das Attribut stets mit dem kompletten Namespace zu notieren, die Kurzform ignoriert Visual Studio.

Eigenschaften zur Entwurfszeit verbergen
Ob eine Eigenschaft zur Entwurfszeit im Eigenschaftenfenster sichbar ist, kann über das Browsable-Attribut gesteuert werden. Möchten Sie darüber hinaus auch verhindern, dass der Entwickler die Eigenschaft in seiner IntelliSense-Liste zu Gesicht bekommt, setzen Sie zusätzlich das EditorBrowsable-Attribut mit dem Wert EditorBrowsableState.Never.

[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public string MyProperty
{
...
}

Komponenten zur Entwurfszeit verbergen
Vielleicht wollen Sie aber auch die komplette Komponente verbergen, sprich sie soll nicht in der Toolbox erscheinen. Hierfür deklarieren Sie auf Klassenebene das ToolboxVisible-Attribut.

[ToolboxItem(false)]
public class MyComponent : Component
{
...
}

Soll die Komponente zwar in der Toolbox erscheinen, nicht jedoch im Component Tray, so setzen Sie das DesignTimeVisible-Attribut auf false.

[DesignTimeVisible(false)]
public class MyComponent : Component
{
...
}

Entwurfszeit-Attribute

Als kleine Gedächtnisstützte für mich, aber auch als Referenz für alle Interessierten, habe ich mal die wichtigsten Entwurfszeitattribute für die WinForms-Komponentenentwicklung zusammengetragen. In den nachfolgenden Artikeln möchte ich jedes dieser Attribute kurz vorstellen und hierbei die folgenden Themen näher beleuchten.