Die Schnittstelle IExtenderProvider

Veröffentlicht: 15. Jan 2003 | Aktualisiert: 09. Nov 2004

Von Thomas Fenske

Visual Studio .NET und das Framework SDK sind nun schon einige Monate auf dem Markt und inzwischen haben wir uns einigermaßen zurechtgefunden. Wir sind wieder in der Lage, Messageboxen auszugeben, Dateien zu bearbeiten, auf Datenbanken zuzugreifen und Umgebungsvariablen auszulesen. Es ist nun an der Zeit, sich den neuen Dingen zuzuwenden, die ein wenig im Verborgenen schlummern und darauf warten, entdeckt zu werden.

Auf dieser Seite

 IExtenderProvider
 MandatoryProvider
 (K)eine separate Eigenschaft für die Klasse Form
 Fazit

dotnet_magazin_logo

Diesen Artikel können Sie dank freundlicher Unterstützung von dot.net magazin auf MSDN Online lesen. dot.net magazin ist ein Partner von MSDN Online.

msdn_partner_logo

Der Namespace System.ComponentModel ist ein Ort, der recht dankbar für Erkundungslustige ist. Hier tummeln sich eine ganze Reihe von Klassen und Interfaces, die bei genauerer Betrachtung faszinierende Dienste bereithalten. Allerdings wird der Zugang zu diesen Diensten oft durch - auf den ersten Blick - verwirrende und komplizierte Schnittstellen erschwert. Am besten nähert man sich dem Ganzen über ein harmlos erscheinendes Interface, das man dann als Brückenkopf für weitere Expeditionen verwenden kann.

Ein ganz besonders sympathisches Exemplar aus System.ComponentModel möchte ich in diesem Artikel einmal genauer unter die Lupe nehmen, das Interface IExtenderProvider.

IExtenderProvider

Was macht dieses Interface so sympathisch? Ganz einfach die Tatsache, dass IExtenderProvider nur eine einzige Methode mit einer geradezu lächerlich simplen Signatur besitzt: bool CanExtend(object extendee). Zwar wird es nicht nur bei der Implementierung dieses einen Interfaces bleiben und einige Dinge werden etwas komplizierter, als sie zunächst scheinen, doch immer hübsch der Reihe nach. Bevor wir unser Basislager aufschlagen, werden wir die Gegend noch etwas erkunden.

Eine Komponente - also eine von System.ComponentModel.Component abgeleitete Klasse -, die zusätzlich das Interface IExtenderProvider erfüllt, ist in der Lage, andere Klassen um Eigenschaften zu erweitern, ohne die jeweiligen Klassen zu verändern oder von diesen abzuleiten. Das .NET Framework beinhaltet mit dem HelpProvider eine sehr nützliche Klasse, die sich dieser Technik bedient. Einmal auf ein Formular gezogen, erweitert der HelpProvider alle Controls unter anderem um die Eigenschaft HelpString (siehe Abb. 1).

Test - Microsoft Visual C# .NET

Abb. 1: Der HelpProvider in Aktion

Zur Laufzeit wird durch Drücken der Taste F1 auf der textBox1 der eingestellte Hilfetext in einem Tooltip angezeigt. Der HelpProvider hat also die Funktionalität der Textbox erweitert. Und nicht nur das. Er hat alle Klassen um diese Funktionalität erweitert, die von der Klasse System.Windows.Forms.Control abgeleitet sind. Vergleichbares ist mit Vererbung gar nicht zu erreichen, weil man in der Vererbungshierarchie eine weitere Ebene unterhalb von Control einfügen müsste, was bekanntlich nicht geht.

 

MandatoryProvider

Wie einfach derartiges zu realisieren ist, wollen wir uns nun in einem kleinen Beispiel ansehen. Jeder kennt diese Online-Formulare, auf denen man bestimmte Felder ausfüllen muss und die einen anschließend darauf aufmerksam machen, dass man vergessen hat dieses oder jenes Felder auszufüllen. Diese Funktionalität werden wir durch einen ExtenderProvider realisieren. Dafür legen wir als erstes ein neues Projekt vom Typ C# Class Library mit dem Namen Extenders an, löschen die dort enthaltene Klasse Class1 und erzeugen stattdessen über ADD COMPONENT eine Klasse mit dem Namen MandatoryProvider die von Component abgeleitet ist. Das um alle überflüssigen Teile bereinigte Ergebnis ist in Listing 1 abgebildet.

Listing 1

using System; 
using System.ComponentModel; 
using System.Collections; 
using System.Diagnostics; 
namespace Extenders 
{ 
public class MandatoryProvider : System.ComponentModel.Component 
{ 
public MandatoryProvider(System.ComponentModel.IContainer container) 
{ 
container.Add(this); 
} 
public MandatoryProvider() 
{ 
} 
} 
}

Nun soll unser MandatoryProvider das Interface IExtenderProvider einschließlich der Implementierung von CanExtend erfüllen. Er soll alle Instanzen der Klassen TextBox, DateTimePicker und NumericUpDown um die Eigenschaft Mandatory erweitern. Die Methode CanExtend trifft allerdings nur die Aussage, ob eine bestimmte Instanz erweitert werden kann oder nicht. Worum die Instanz erweitert werden soll, wird nicht über IExtenderProvider ausgedrückt, sondern über die Attribute ProvideProperty und ExtenderProvidedProperty.

ProvideProperty wird vor die Klasse geschrieben und zählt alle Eigenschaften auf, um die der ExtenderProvider die Klassen erweitert. Außerdem wird mit diesem Attribut definiert, welchen Typ die erweiterten Klassen gemeinsam haben. In unserem Fall genügt also [ProvideProperty("Mandatory", typeof(Control))]. Die Implementierung der Eigenschaft Mandatory erfolgt nun über die Methoden GetMandatory und SetMandatory. Beide Methoden werden zusätzlich mit dem parameterlosen Attribut ExtenderProviderProperty gekennzeichnet.

Die Namenskonvention SetEigenschaftname und GetEigenschaftsname ist unbedingt einzuhalten. Dies ist für das .NET Framework etwas untypisch. Normalerweise werden solche Zusammenhänge ausschließlich über Attribute definiert. Die Erwartungshaltung wäre also gewesen, bei ExtenderProviderProperty den bei ProvideProperty angegebenen Namen zu wiederholen. Diese offensichtliche Designschwäche wirkt sich allerdings nicht weiter negativ aus. Nach all diesen Änderungen sieht der MandatoryProvider nun aus wie in Listing 2. Alle Änderungen sind fett markiert.

Listing 2

using System; 
using System.ComponentModel; 
using System.Collections; 
using System.Diagnostics; 
using System.Windows.Forms; 
namespace Extenders 
{ 
[ProvideProperty("Mandatory", typeof(Control))] 
public class MandatoryProvider : System.ComponentModel.Component, IExtenderProvider 
{ 
public MandatoryProvider(System.ComponentModel.IContainer container) 
{ 
container.Add(this); 
} 
public MandatoryProvider() 
{ 
} 
#region Implementation of IExtenderProvider 
bool IExtenderProvider.CanExtend(object extendee) 
{ 
if (extendee is TextBox) 
return true; 
if (extendee is DateTimePicker) 
return true; 
if (extendee is NumericUpDown) 
return true; 
if (extendee is Form) 
return true; 
return false; 
} 
[ExtenderProvidedProperty] 
public bool GetMandatory(Control control) 
{ 
return false; 
} 
[ExtenderProvidedProperty] 
public void SetMandatory(Control control, bool value) 
{ 
} 
#endregion 
} 
}

Die Signaturen der Get- und Set-Methoden orientieren sich natürlich an den Typen der neuen Eigenschaften. In diesem Fall ist das bool. Der ExtenderProvider ist nun fast vollständig. Allerdings speichert er die Einstellungen für die jeweiligen Controls noch nicht. Diese Informationen können wir in einer ArrayList abspeichern. Jedes Control, bei dem Mandatory auf true gesetzt wird, wird in diese Liste aufgenommen. Die neue Implementierung der Methoden GetMandatory und SetMandatory entspricht dann der Darstellung in Listing 3.

Listing 3

private ArrayList extendees_ = new ArrayList(); 
[ExtenderProvidedProperty] 
public bool GetMandatory(Control control) 
{ 
return extendees_.Contains(control); 
} 
[ExtenderProvidedProperty] 
public void SetMandatory(Control control, bool value) 
{ 
if (value) 
extendees_.Add(control); 
else 
if (extendees_.Contains(control)) 
extendees_.Remove(control); 
}

Geschafft! Die Grundstruktur unseres ExtenderProvider steht. Jetzt müssen wir uns nur noch um seine eigentliche Aufgabe kümmern. Wenn versucht wird, einen Dialog zu schließen, auf dem sich leere Pflichtfelder befinden, sollen das Schließen verhindert und die beanstandeten Felder markiert werden. Das benötigte Ereignis ist das Closing-Ereignis des Formulars, auf dem sich die Controls befinden. Zur Markierung eines leeren Controls bietet sich der Einsatz eines ErrorProvider an. Alles was wir tun müssen, ist beim Aufruf von SetMandatory dafür zu sorgen, dass beim Formular des jeweiligen Controls auf das Closing-Ereignis reagiert wird. Überraschenderweise liefert Control.TopLevelControl den Wert null. Woran liegt's? Sehen wir uns dazu den Code an, den der Formular-Designer von Visual Studio .NET in die Methode InitializeComponent generiert.

Listing 4

private void InitializeComponent() 
{ 
this.components = new System.ComponentModel.Container(); 
this.mandatoryProvider1 = new Extenders.MandatoryProvider(this.components); 
this.textBox1 = new System.Windows.Forms.TextBox(); 
this.SuspendLayout(); 
//  
// textBox1 
//  
this.textBox1.Location = new System.Drawing.Point(16, 24); 
this.mandatoryProvider1.SetMandatory(this.textBox1, true); 
this.textBox1.Name = "textBox1"; 
this.textBox1.Size = new System.Drawing.Size(152, 20); 
this.textBox1.TabIndex = 0; 
this.textBox1.Text = "textBox1"; 
//  
// Form1 
//  
this.AutoScaleBaseSize = new System.Drawing.Size(5, 13); 
this.ClientSize = new System.Drawing.Size(292, 273); 
this.Controls.AddRange(new System.Windows.Forms.Control[] {this.textBox1}); 
this.Name = "Form1"; 
this.Text = "Form1"; 
this.ResumeLayout(false); 
}

Wie man an den hervorgehobenen Zeilen in Listing 4 sieht, wird der MandatoryProvider zu einem Zeitpunkt aufgerufen, wo das jeweilige Control noch gar nicht bei seinem Vater registriert ist. Rettung verspricht hier das Ereignis ParentChanged des Controls. Also registrieren wir uns zunächst dort und melden uns erst bei der Zuordnung des Vaters bei dem Ereignis Closing des Formulars an. Das funktioniert genau so lange, wie wir die Controls nicht direkt auf dem Formular platzieren, sondern auf einem andern Control wie z.B. einer GroupBox, einem Panel oder einem TabControl.

Die Zuordnung zum Formular geschieht als letztes. Das Ereignis ParantChanged wird also gefeuert, bevor der Vater selbst bei dem Formular oder dessen Vater angemeldet wird. Ich habe mir den Spaß gemacht und das Ganze so implementiert, dass sich der MandatoryProvider in diesem Fall bei dem ParentChanged-Ereignis des jeweils höchsten Vaters anmeldet und wartet, bis dieser beim Formular oder einem weiteren Control angemeldet wird.

Das Ergebnis war ein fürchterlich langer Verwaltungszirkus, der etwa zwei Drittel des gesamten Codes einnahm und mich insgesamt nicht besonders zufrieden stellte. Eine andere Lösung besteht darin, dass der MandatoryProvider auch das Formular um eine Eigenschaft erweitert. Dadurch wird SetMandatory für das Formular aufgerufen und wir können uns dort direkt bei dem Closing-Ereignis registrieren (siehe Listing 5).

Listing 5

[ProvideProperty("Mandatory", typeof(Control))] 
public class MandatoryProvider : System.ComponentModel.Component, IExtenderProvider 
{ 
private ArrayList extendees_ = new ArrayList(); 
private ErrorProvider errorProvider_ = new ErrorProvider(); 
private Form form_; 
public MandatoryProvider(System.ComponentModel.IContainer container) 
{ 
container.Add(this); 
} 
public MandatoryProvider() 
{ 
} 
#region Implementation of IExtenderProvider 
bool IExtenderProvider.CanExtend(object extendee) 
{ 
if (extendee is TextBox) 
return true; 
if (extendee is DateTimePicker) 
return true; 
if (extendee is NumericUpDown) 
return true; 
if (extendee is Form) 
return true; 
return false; 
} 
[ExtenderProvidedProperty] 
public bool GetMandatory(Control control) 
{ 
Form form = control as Form; 
if (form != null) 
{ 
return !(form_ == null); 
} 
return extendees_.Contains(control); 
} 
[ExtenderProvidedProperty] 
public void SetMandatory(Control control, bool value) 
{ 
Form form = control as Form; 
if (form != null) 
{ 
if (form_ != null && !DesignMode) 
form_.Closing -= new CancelEventHandler(OnCheckMandatoryControls); 
form_ = value ? form : null; 
errorProvider_.ContainerControl = form_; 
if (form_ != null && !DesignMode) 
form.Closing += new CancelEventHandler(OnCheckMandatoryControls); 
return; 
} 
if (value) 
extendees_.Add(control); 
else 
if (extendees_.Contains(control)) 
extendees_.Remove(control); 
} 
private void OnCheckMandatoryControls(object sender, CancelEventArgs e) 
{ 
// TODO: check empty controls 
} 
#endregion 
}

 

(K)eine separate Eigenschaft für die Klasse Form

Natürlich wäre es schöner, für das Formular eine vollkommen neue Eigenschaft wie etwa CheckMandatoryControls zu vergeben und dafür die Eigenschaft Mandatory nur bei den einzelnen Controls anzubieten. Prinzipiell ist das auch möglich. Um etwa nur die Klasse Form um eine Eigenschaft zu erweitern, gibt man als Typ bei dem ProvideProperty-Attribut die Klasse Form an ([ProvideProperty("CheckMandatoryControls", typeof(Form))]). So weit so gut. Dummerweise ist die Klasse Form auch von der Klasse Control abgeleitet und Control ist die erste gemeinsame Klasse von TextBox, DateTimePicker und NumericUpDown. Sobald also eine Eigenschaft für die Klasse Control definiert ist, besitzen auch alle Instanzen der Klasse Form diese Eigenschaft.

Mein letzter Versuch bestand darin, das PropertyProvide-Attribut für die Eigenschaft Mandatory dreimal zu verwenden und jeweils einen der Typen TextBox, DateTimePicker und NumericUpDown anzugeben. Aber leider war auch das zum Scheitern verurteilt, denn offensichtlich wird in diesem Fall lediglich eines der Attribute ausgewertet. Damit steht aber nur bei einem Control-Typ die Eigenschaft Mandatory zur Verfügung. Somit muss es also bei der in Listing 5 gezeigten Implementierung bleiben, bei der nur noch der Code für OnCheckMandatoryControls fehlt. Dieser ist jedoch recht unspektakulär, wie Listing 6 zeigt.

Listing 6

private void OnCheckMandatoryControls(object sender, CancelEventArgs e) 
{ 
form_.SuspendLayout(); 
foreach(Control control in extendees_) 
{ 
bool cancel = false; 
errorProvider_.SetError(control, ""); 
if (control is TextBox) 
{ 
if (control.Text.Equals("")) 
cancel = true; 
} 
else if (control is DateTimePicker) 
{ 
DateTimePicker picker = control as DateTimePicker; 
if (picker.ShowCheckBox && !picker.Checked) 
cancel = true; 
} 
else if (control is NumericUpDown) 
{ 
NumericUpDown numeric = control as NumericUpDown; 
if (numeric.Value == 0) 
cancel = true; 
} 
if (cancel) 
{ 
e.Cancel = true; 
errorProvider_.SetError(control, "Diese Feld muss ausgefüllt werden."); 
} 
} 
form_.ResumeLayout(); 
}

Damit haben wir unsere Expedition erfolgreich abgeschlossen und können die Früchte unserer Arbeit in Abbildung 2 begutachten. Das Formular kann erst geschlossen werden, wenn alle Pflichtfelder ausgefüllt sind. Die Prüfungen werden allerdings nur durchgeführt, wenn auch bei dem Formular selbst die Eigenschaft Mandatory auf true gesetzt wurde.

Form1

Abb. 2: Der MandatoryProvider in Aktion

 

Fazit

Neben der Möglichkeit, sehr einfach Subklassen von Controls abzuleiten, bietet das .NET Framework mit dem ExtenderProvider-Mechanismus ein hervorragendes Werkzeug, um Steuerungsabläufe, die in jedem Dialog wiederkehren, sauber zu kapseln und wieder zu verwenden. Der Codeumfang der chronisch langen Dialog-Klassen wird dadurch massiv reduziert, was sich auf die Wartbarkeit nicht gerade schädlich auswirkt. Außerdem wird durch die Wiederverwendung ein einheitliches Verhalten aller Dialoge innerhalb des Softwaresystems erreicht.

Bekanntlich ist dort, wo Licht ist, auch Schatten. Bei der Realisierung konkreter Aufgaben begleiten uns die vorgestellten Hilfsmittel nur einen Stück des Weges. Der Zugriff auf das Formular eines Controls hat das verdeutlicht. Dennoch hoffe ich, Sie auf die vielfältigen Neuerungen neugierig gemacht zu haben, die das .NET Framework insbesondere in dem Namespace System.ComponentModel bietet. Viel Spaß bei Ihren eigenen Entdeckungsreisen!