head.WriteLine()

Montag, Oktober 02, 2006

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.