Sebastians Weblog

ESCde Developer Blog

  Home :: Kontakt :: RSS Feed
  3 Posts :: 0 Artikel :: 7 Kommentare :: 0 Trackbacks

Archiv

Post Kategorien

ESCde

ESCde Blogger

Unter folgendem Link können Sie diesen Blog Eintrag als Word Dokument finden: EntwurfsFunktionalität.doc
Einleitung
 
Die Entwicklung von Steuerelementen wurde durch das .net Framework stark vereinfacht. Man leitet seinen Anforderungen entsprechend eine Klasse von UserControl oder einem anderen Steuerelement, das benötigte Grundfunktionalitäten bietet, ab. Anschließend erweitert man diese Klasse. Um ein Steuerelement allerdings erst so richtig komfortabel nutzen zu können braucht es etwas mehr: hierfür ist eine gewisse Funktionalität zur Entwurfszeit nützlich, wie sie auch von den bereits mit dem Framework mitgelieferten Steuerelementen geboten wird. Zu diesen Annehmlichkeiten gehören unter anderem Smart Tags (siehe Abb. 1) oder übersichtliche Dialogfenster, mit denen man zur Entwurfszeit komfortabel Eigenschaften eines Steuerelements konfigurieren kann (z.B. wie bei der Mask - Eigenschaft einer MaskedTextBox). Dieses White Paper wird die Implementierung solcher Entwurfszeit – Funktionalitäten anhand eines Beispiels erläutern.
 
 
Abbildung 1 - Schneller Zugriff auf Eigenschaften durch Smart Tags, hier bei einem DataGridView Steuerelement.
 
TypeConverters
 
Das Beispiel das wir in diesem White Paper entwickeln werden, ist ein simples Steuerelement zur Anzeige und Abgabe von Bewertungen (RatingControl). Diese Bewertung wird standardmäßig in Form von Sternen angezeigt, wie man es z.B. vom Windows Media Player her kennt. Man kann das verwendete Bild auch verändern (wie in Abbildung 3 zu sehen ist). Ferner kann man den maximalen Wert einstellen. Wir kapseln diese Eigenschaften alle in einer Klasse namens Rating.
 
 
                                                                                  
Abbildung 2 - RatingControl,                                 Abbildung 3 - RatingControl,
                       Standard Aussehen                                            „bombiges“ Aussehen
 
 
Wie man sehen kann, umfasst die Rating – Klasse folgende Eigenschaften: RatingImage, Maximum und RatingValue (die eigentliche Bewertung):
 
public class Rating
{
        private static Image starImage = Resources.star;  //standard bild
 
        private Image ratingImage = Rating.starImage;
        private int maximum       = 5;
        private int ratingValue   = 0;
     
        public event EventHandler RatingConfigurationChanged;
        public event EventHandler RatingValueChanged;
 
        public int RatingValue
        {
            get { return ratingValue; }
            set
            {
                if (ratingValue < 0)
                {
                throw new ArgumentException("RatingValue musn't be < 0.");
                }
 
                CheckRatingAndMaximum(value, this.maximum);
                this.ratingValue = value;
                if (this.RatingValueChanged != null)
                {
                    this.RatingValueChanged(this, EventArgs.Empty);
                }
            }
        }
 
        public int Maximum //maximale Bewertung
        {
            get { return maximum; }
            set
            {
                if (value < 1)
                {
                    throw new ArgumentException("Value musn't be < 1.");
                }
 
                CheckRatingAndMaximum(this.ratingValue, value);
                maximum = value;
                OnRatingConfigChanged();
            }
        }
 
        public Image RatingImage //Das Bild für die Anzeige der Bewertung
        {
            get { return ratingImage; }
            set
            {
                if (value != null)
                    ((Bitmap)value).MakeTransparent();
 
                ratingImage = value;
                OnRatingConfigChanged();
            }
         }
 
        ///<summary>
        /// Checks that RatingValue isn't greater than Maximum value.
        ///</summary>
        private void CheckRatingAndMaximum(int proposedRating, int
proposedMaximum)
        {
            if (proposedRating > proposedMaximum)
            {
                throw new ArgumentException("RatingValue can't be greater
than Maximum.");
            }
         }
 
 
      // …
}
        
Wenn wir jetzt diese Rating- Klasse als Typ einer Eigenschaft namens Rating im Steuerelement nutzen…
 
public partial class RatingControl : UserControl
{
private Rating rating = new Rating();
 
      public Rating Rating
      {
            get { return rating; }
            set
            {
// …
 
                rating = value;
 
                  // …
            }
      }
 
      // …
 
}
 
…und dann versuchen dieses Steuerelement in Visual Studio zu verwenden, bemerken wir ziemlich schnell, dass die Rating Eigenschaft im Eigenschaften-Fenster überhaupt nicht verändert werden kann (Abb. 4).
 
 
Abbildung 4Rating - Eigenschaft kann nicht verändert werden
 
 
Diese Problematik tritt allerdings nicht bei allen komplexeren Datentypen auf, so gibt es z.B.
für Image, Size, enums, int etc. bereits TypeConverters. TypeConverters sind Klassen die dabei helfen, die Instanz einer Klasse (die als Typ für eine Eigenschaft des Steuerelements fungiert) zu konvertieren, so dass sie im Eigenschaften- Fenster angezeigt und verändert werden kann. Meistens wird hierfür zu einem String und zurück konvertiert. Für unsere Rating Klasse gibt es (noch) keinen TypeConverter, daher kann Visual Studio die Instanz nicht entsprechend konvertieren, mit dem Ergebnis, dass wir die Eigenschaft im Eigenschaften Fenster nicht konfigurieren können. Um das zu ändern, benötigen wir nun eine Klasse, die wir von ExpandableObjectConverter (System.ComponentModel.Design) ableiten:  
 
public class RatingTypeConverter : ExpandableObjectConverter
{ }
 
Wir leiten hier von ExpandableObjectConverter und nicht von TypeConverter ab, damit man im Eigenschaften- Fenster die Eigenschaft „aufklappen“ kann, und die einzelnen Werte verändern kann, so wie z.B. bei Size den Height und Width Wert (Abbildung 5).
 
 
Abbildung 5 – die Size Eigenschaft „aufgeklappt“
 
 
Nun überschreiben wir die CanConvertTo Methode in dieser Klasse, und geben true zurück wenn destinationType vom Typ String ist. Damit signalisieren wir Visual Studio, dass die Klasse fähig ist, die Rating Instanz zu einem String zu konvertieren.
 
public override bool CanConvertTo(ITypeDescriptorContext context, Type
destinationType)
{
if (destinationType.GetType() == typeof(string))
      {
            return true;
      }
      else
      {
            return base.CanConvertTo(context, destinationType);
      }
}
 
Genau dies machen wir dann auch, indem wir die ConvertTo Methode überschreiben:
 
public override object ConvertTo(ITypeDescriptorContext context,
System.Globalization.CultureInfo culture, object value, Type destinationType)
{ }
 
Nun wird die Rating Instanz (im Argument value gegeben) zu einen String konvertiert. Hierfür wandeln wir jeweils die einzelnen Werte der Eigenschaften in einen String um und verbinden diese mit Semikola:   
           
StringBuilder stb = new StringBuilder();               
stb.Append(rating.Maximum.ToString()).Append(";");
stb.Append(rating.RatingValue.ToString()).Append(";");
 
Um das RatingImage in einen String zu konvertieren nutzen wir die ImageFormatConverter Klasse (System.Drawing). Anschließend geben wir den String zurück. Hier nun der ganze Inhalt der ConvertTo Methode:
 
if (destinationType.GetType() == typeof(string))
{
Rating rating = (Rating) value;
 
ImageFormatConverter imageConverter = new ImageFormatConverter();
               
StringBuilder stb = new StringBuilder();               
stb.Append(rating.Maximum.ToString()).Append(";");
stb.Append(rating.RatingValue.ToString()).Append(";");
 
      if (rating.RatingImage == null)
      {
      stb.Append("null");
      }
      else
{       //convert rating image and add    
stb.Append(imageConverter.ConvertToInvariantString(context,
rating.RatingImage)); 
      }
 
      return stb.ToString();
}
else
{
return base.ConvertTo(context, culture, value, destinationType);
}
 
Nun möchten wir von einem String wieder zu einer Rating Instanz zurück konvertieren. Hierzu überschreiben wir die CanConvertFrom Methode und implementieren diese analog zur CanConvertTo Methode. Im nächsten Schritt überschreiben wir die ConvertFrom Methode:
 
public override object ConvertFrom(ITypeDescriptorContext context,
System.Globalization.CultureInfo culture, object value)
{ }
 
Nun formen wir einen String, der unsere Rating Instanz enthält (wieder das Argument value), zurück zur Rating Instanz um. Zuerst splitten wir den String bei den Semikola, um die Werte der einzelnen Eigenschaften als Strings zu erhalten:
               
string stringValue = (string)value;
string[] values     = stringValue.Split(';');
 
Diese einzelnen Strings konvertieren wir in ihren ursprünglichen Typ zurück und setzen damit die Eigenschaften einer neuen Rating Instanz. Anschließend geben wir besagte Instanz zurück. Auch hier verwenden wir wieder die ImageFormatConverter Klasse um von einem String zurück zu Image zu konvertieren. Hier der vollständige Inhalt der ConvertFrom Methode:
 
if (value.GetType() == typeof(string))
{
string stringValue = (string)value;
 
//values sind Werte der einzelnen Eigenschaften
      string[] values     = stringValue.Split(';');
      Rating returnRating = new Rating();
 
      returnRating.Maximum     = int.Parse(values[0]);
      returnRating.RatingValue = int.Parse(values[1]);
 
      ImageFormatConverter imageConverter = new ImageFormatConverter();
 
      if (values[2] == "null")
      {
            returnRating.RatingImage = null;
      }
       else
       {
            returnRating.RatingImage = (Image)imageConverter.
ConvertFromInvariantString(context, values[2]);
       }
 
return returnRating;
}
 
 
Nun fügen wir zusätzlich noch das TypeConverterAttribut mit unserer RatingTypeConverter Klasse als Argument zur Rating Klasse hinzu:
 
[TypeConverter(typeof(RatingTypeConverter))]
public class Rating
{ }
 
Voila, nun kann Visual Studio die Rating Klasse entsprechend konvertieren, und wir damit die Rating Eigenschaft unseres Steuerelements bequem im Eigenschaften Fenster nutzen und verändern, wie man in Abbildung 6 sehen kann.
 
 
Abbildung 6 - die einzelnen Werte der Rating Eigenschaft können nun im Eigenschaften- Fenster verändert werden
 
 
Bei dem Steuerelement das wir hier nutzen, ist die Implementierung einer extra Klasse für die Eigenschaften (was ja mittels der Rating Klasse gemacht wurde) wahrscheinlich nicht sehr sinnvoll. Bei anderen Steuerelementen ist es jedoch häufig der Fall, dass man Eigenschaften mit eigenen Klassen hat. Um diese Eigenschaften dann entsprechend im Eigenschaften Fenster nutzen zu können, muss man dann, wie oben beschrieben, für die eigenen Klassen entsprechende TypeConverters implementieren, wobei sich ExpandableObjectConverter als Basisklasse hierfür bestens eignet.
 
Dialoge zur Entwurfszeit
 
Um die Rating Eigenschaft des Steuerelementes noch komfortabler konfigurieren zu können bietet es sich an, einen Dialog dafür im Eigenschaften Fenster anzuzeigen, wie es bei anderen Steuerelementen, z.B. bei der Mask Eigenschaft der MaskedTextBox, auch der Fall ist. Gerade bei Eigenschaften, die umständlich zu konfigurieren sind, oder wo viele Werte eingestellt werden müssen, kann ein Dialog zur Entwurfszeit eine wertvolle Unterstützung sein. Zuerst leitet man hierfür eine Klasse von UITypeEditor (System.Drawing.Design) ab:
 
public class RatingEditor : UITypeEditor
{ }
 
Im Anschluss daran überschreiben wir die GetEditStyle Methode, und geben UITypeEditorEditStyle.Modal zurück:
 
public override UITypeEditorEditStyle
GetEditStyle(System.ComponentModel.ITypeDescriptorContext context)
{
return UITypeEditorEditStyle.Modal;
}
 
Damit veranlassen wir, dass neben der Rating Eigenschaft im Eigenschaften Fenster ein Button angezeigt wird (Abbildung 7). Durch klicken auf diesen Button wird dann unser Dialog angezeigt werden.
 
 
Abbildung 7 – rechts neben der Eigenschaft der Button für unseren Dialog
 
 
Falls man anstatt eines Dialoges ein Drop-Down Steuerelement anzeigen will (wie z.B. bei Eigenschaften die ein enum als Typ haben), würde man hier stattdessen UITypeEditorEditStyle.DropDown zurückgeben. Um nun unseren Dialog anzeigen zu können, überschreiben wir die EditValue Methode:
 
public override object EditValue(System.ComponentModel.ITypeDescriptorContext context,
IServiceProvider provider, object value)
{ }
 
In dieser Methode öffnen wir unseren Dialog und bearbeiten den Wert der Eigenschaft. Dieser Wert befindet sich im value Argument der Methode. Anschließend geben wir unseren ggf. veränderten Wert zurück:
 
public override
object EditValue(System.ComponentModel.ITypeDescriptorContext context,
IServiceProvider provider, object value)
{
 
return this.ShowRatingConfigurationDialog((Rating)value);
}
 
public Rating ShowRatingConfigurationDialog(Rating rating)
{
RatingConfigurationDialog dlg = new
 RatingConfigurationDialog(rating.Clone());
 
      if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK)
      {
            return dlg.Rating;
      }
 
      return rating;
}
 
Zum Schluss fügen wir nun abermals ein Attribut, in diesem Fall das EditorAttribute, zur Rating Klasse hinzu, und übergeben als Argument unsere RatingEditor Klasse und deren Basisklasse, also UITypeEditor:
 
[Editor(typeof(RatingEditor), typeof(UITypeEditor))]
[TypeConverter(typeof(RatingTypeConverter))]
public class Rating { /* ... */ }
 
Nun können wir unseren Dialog nutzen, um eine Rating Instanz (in diesem Fall die Rating Eigenschaft des Steuerelements) im Eigenschaften Fenster zu konfigurieren, wie man in Abbildung 8 sehen kann.
 
 
Abbildung 8 – Konfiguration der Rating Eigenschaft via Dialog
 
 
 
Es ist im Allgemeinen empfehlenswert das zu konfigurierende Steuerelement zum Dialog hinzuzufügen, damit man Veränderungen sofort sehen kann, und ggf. abbrechen kann. Die Veränderungen sofort sehen zu können ist gerade praktisch wenn viele Werte einzustellen sind. Zum Schluss sei noch erwähnt, dass das .NET Framework einige alltäglich verwendete Klassen besitzt, die bereits mit eigenen Editoren ausgestattet sind, wie z.B. Font, Image und Color. Dies bedeutet, dass sie für diese nicht ihre eigenen Editoren implementieren müssen (für weitere Informationen zu den Editoren dieser Klassen siehe http://msdn2.microsoft.com/de-de/library/bfc7teys.aspx).
 
 
Smart Tags
 
Smart Tags wurden mit Visual Studio 2005 neu eingeführt, zumindest was ihre Verwendung in IDEs anbelangt (sie existieren schon seit Office XP). Wie bereits in der Einleitung erwähnt ermöglichen sie den schnellen Zugriff auf Funktionen zum konfigurieren eines Steuerelements. Hierfür können auf der Smart Tag Panele 4 Arten von Elementen angezeigt werden: einfachere wie Kategorien (zur Unterteilung der Panele) und Text (Abb.9), sowie Kommandos (werden als Link dargestellt – siehe Abb. 10) oder Eigenschaften. Eigenschaften werden genauso angezeigt wie im Eigenschaften Fenster (Abb. 11), auch hierbei spielen Editoren wieder eine Rolle.
 
Abbildung 9 – Kategorie und Text auf der Smart Tag Panele
 
 
Abbildung 10 – Kommando auf der Smart Tag Panele
 
 
Abbildung 11 – Eigenschaft auf der Smart Tag Panel, hier die RatingImage Eigenschaft
 
 
Wir fangen damit an, dass wir definieren was alles an Befehlen auf der Smart Tag Panele auftauchen soll. Wir erreichen dies, indem wir eine Klasse von DesignerActionList  (System.ComponentModel.Design,  in der System.Design.dll) ableiten:
 
public class RatingActionList : DesignerActionList
{ }
 
Danach definieren wir einen Konstruktor, der ein Argument vom Typ RatingControl hat, das Steuerelement für das wir Smart Tag nutzen wollen. Dieses Argument übergeben wir an den Konstruktor der Basisklasse und legen zudem noch eine Referenz davon als privates Feld an:
 
private RatingControl control = null;
 
public RatingActionList(RatingControl control) : base (control)
{
this.control = control;
}
 
Nun implementieren wir die Elemente. Für Kommandos implementieren wir Methoden, wie die ConfigureRating Methode hier, die den gleichen Dialog aufruft, den wir schon oben genutzt haben:
 
public void ConfigureRating()
{
RatingEditor editor = new RatingEditor();
      Rating rating = editor.
ShowRatingConfigurationDialog(this.control.Rating);
           
PropertyDescriptor propDescriptor =
TypeDescriptor.GetProperties(this.control)["Rating"];
      propDescriptor.SetValue(this.control, rating);
}
 
Das einzige was hierbei zu beachten ist, ist dass die Werte des Steuerelements nicht direkt verändert werden sollten, sondern stattdessen wie oben im Quellcode über die PropertyDescriptor Klasse:
 
PropertyDescriptor propDescriptor = TypeDescriptor.
GetProperties(this.control)["Rating"];
propDescriptor.SetValue(this.control, rating);
 
Ohne die Verwendung von PropertyDescriptor bekommt die Visual Studio Infrastruktur die Veränderung des Steuerelementes nicht mit, was zur Folge hat, dass man z.B. den „Rückgängig“ Befehl in Visual Studio nicht nutzen kann um diese Veränderungen rückgängig machen zu können. Die eigentlichen Veränderungen am Steuerelement würden natürlich auch ohne PropertyDescriptor dennoch stattfinden. Ansonsten sind sie ziemlich frei in dem was sie innerhalb der Methode implementieren. Um diese Methode nun als Kommando auf der Smart Tag Panele angezeigt zu bekommen überschreiben wir die GetSortedActionItems Methode:
 
public override DesignerActionItemCollection GetSortedActionItems()
{ }
 
In dieser Methode erzeugen wir eine DesignerActionItemCollection Instanz.
 
DesignerActionItemCollection collection =
new DesignerActionItemCollection();
 
Zu dieser collection werden alle Elemente hinzugefügt, die wir auf der Smart Tag Panele haben wollen. Für unsere ConfigureRating Methode von oben brauchen wir eine DesignerActionMethodItem Instanz. Im Konstruktor des DesignerActionMethodItem gibt man zuerst eine Instanz der DesignerActionList Klasse an, danach den Namen der Methode die beim Klicken des Kommandos aufgerufen wird (achten sie darauf, dass sie den Methodennamen absolut korrekt schreiben. Da dieser als String übergeben wird, würde sich ein Fehler beim Kompilieren nicht bemerkbar machen). Dem folgen ein kurzer Text, der auf der Panele als Link angezeigt wird für das Kommando, die Kategorie des Kommandos sowie eine Beschreibung die ggf. als Tooltip auftaucht. Zuletzt kommt ein Argument vom Typ bool. Dieses bestimmt, ob das Kommando auch als DesignerVerb verwendet werden soll (also zur Entwurfszeit auch im Kontextmenü des Steuerelementes verfügbar sein soll):
 
collection.Add(new DesignerActionMethodItem(this, "ConfigureRating",
               "Configure...", "", "Configure Rating property...", true));
 
 
Wie man oben sieht, fügen wir diese DesignerActionMethodItem Instanz zur collection hinzu. Um jetzt zusätzlich noch eine Eigenschaft auf der Smart Tag Panele anzuzeigen fügt man der Klasse eine Property hinzu, in diesem Fall RatingImage:
 
public Image RatingImage
{
get
      {
      return this.control.Rating.RatingImage;
      }
      set
      {
            PropertyDescriptor propDescriptor = TypeDescriptor.
GetProperties(this.control.Rating)["RatingImage"];
            propDescriptor.SetValue(this.control.Rating, value);
      }
}
 
 
Auch hier nutzen wir wieder wie oben in der Methode die PropertyDescriptor Klasse. Ansonsten gibt es keine weiteren Besonderheiten. Um diese RatingImage Eigenschaft der Smart Tag Panele hinzuzufügen, fügen wir der collection von oben noch eine DesignerActionPropertyItem Instanz hinzu:
 
collection.Add(new DesignerActionPropertyItem("RatingImage", "RatingImage"));
 
Das erste Argument des DesignerActionPropertyItem Konstruktors ist der Name der Eigenschaft auf die zurückgegriffen wird. Das zweite Argument ist Text, der für dieses Element auf der Smart Tag Panele angezeigt wird (vor dem eigentlichen Steuerelement was zum Anzeigen und Verändern der Eigenschaft verwendet wird). Wie die Eigenschaft auf der Smart Tag Panele verändert werden kann, wird wieder über Editoren bestimmt (also analog zu oben - „3. Dialoge zur Entwurfszeit“). Doch da die Klasse Image bereits über einen eigenen Editor verfügt, brauchen wir uns hierum nicht zu kümmern.
 
Um eine Kategorie zur Smart Tag Panele hinzuzufügen, fügt man eine DesignerActionHeaderItem Instanz mit dem Namen der Kategorie zur collection hinzu:
 
collection.Add(new DesignerActionHeaderItem("Control Information"));
 
Will man Text (hier als Beispiel die Size – Werte des Steuerelementes) zur Smart Tag Panele hinzufügen nimmt man dazu eine DesignerActionTextItem Instanz, mit dem Text und der Kategorie zu der dieses Element gehört als Konstruktorargumente und fügt diese Instanz zur collection hinzu:
 
collection.Add(new DesignerActionTextItem(String.Format("Size: {0} x {1}",
   this.control.Height, this.control.Width), "Control Information"));
 
Nachdem wir nun alle Elemente die wir auf der Smart Tag Panele haben wollen zur collection hinzugefügt haben, geben wir diese anschließend zurück:
 
return collection;  
 
Es gibt übrigens bei allen oben aufgeführten Klassen überladene Kontruktoren, auch einige die es zulassen, dass man jedem Element eine Kategorie zuordnet (was wir nur bei dem DesignerActionTextItem gemacht haben). Zusätzlich sei noch erwähnt, dass die Elemente auf der Smart Tag Panele in der gleichen Reihenfolge auftauchen werden, wie sie der collection hinzugefügt wurden. Als letzten Schritt leiten wir nun eine Klasse von ControlDesigner (System.Windows.Forms.Design) ab:
       
public class RatingControlDesigner : ControlDesigner
{ }
 
Hier überschreiben wir die ActionLists Eigenschaft:
 
 
public override DesignerActionListCollection ActionLists
{
get{ }
}
 
In dem get Accessor der Eigenschaft instanzieren wir eine Instanz vom Typ DesignerActionListCollection und fügen dieser collection eine Instanz unserer RatingActionList Klasse von oben hinzu. Anschließend geben wir die collection zurück:
 
DesignerActionListCollection collection = new
 DesignerActionListCollection();
 
collection.Add(new RatingActionList((RatingControl) this.Control));
 
return collection;
 
 
Nun kommt zum Schluss mal wieder ein Attribut, diesmal das DesignerAttribut, das wir mit dem RatingControlDesigner als Argument zur RatingControl Klasse hinzufügen:
 
 
[Designer(typeof(RatingControlDesigner))]
public partial class RatingControl : UserControl
{ /* ... */ }
 
 
Nach dieser langen Durststrecke ohne Bilder können wir nun endlich die Smart Tag Panel unseres Steuerelementes in Abbildung 12 sehen.
 
 
 
Abbildung 12 – das RatingControl mit Smart Tag Panele
 
 
Optimierungen
 
Nutzt man das Steuerelement auf einer Form in Visual Studio und schaut sich die vom Visual Studio Designer generierte InitializeComponent Methode an, so stellt man fest, dass Visual Studio jeder neuen Eigenschaft unseres Steuerelementes ein Wert zuweist, selbst wenn wir den Wert dieser Eigenschaft nicht im Eigenschaften Fenster verändert haben:
 
// …
 
this.ratingControl3.Name = "ratingControl3";
rating3.Maximum = 5;
rating3.RatingImage = ((System.Drawing.Image)
(resources.GetObject("rating3.RatingImage")));
rating3.RatingValue = 0;
this.ratingControl3.Rating = rating3;
 
// …
 
Wie man sehen kann sind das alles die Werte, die wir im Konstruktor bzw. bei der Initialisierung der Rating Klasse bereits festgelegt haben. Dadurch dass Visual Studio dennoch den Eigenschaften diese Werte nochmals zuweist, werden Ressourcen verschwendet und die Performance kann darunter leiden, da meistens innerhalb des Steuerelementes hierdurch einige Methoden ausgeführt werden, die entsprechend der Werte das Steuerelement neu konfigurieren. Um Visual Studio nun zu informieren, wann es den Wert der Eigenschaft neu festzulegen hat und wann nicht, bedient man sich des DefaultValue Attributes für die Eigenschaften:
 
[DefaultValue(5)]
public int Maximum
{
// …       
}
 
Solange der Wert von Maximum nun 5 ist, wird Visual Studio in der InitializeComponent Methode keine Quellcodezeile dafür anlegen um der Eigenschaft einen Wert zuzuweisen. Der einzige Nachteil dieses Attributes ist, dass man damit nur konstante Ausdrücke festlegen kann, also es nicht im Zusammenhang mit Objekten (Referenztypen) nutzen kann. Jedoch gibt es auch hierfür eine Lösung: man fügt der Klasse eine ShouldSerializeXXX Methode hinzu, wobei XXX der Name der Eigenschaft ist. Wir brauchen das hier für unsere RatingImage Eigenschaft:
 
private bool ShouldSerializeRatingImage()
{
return !(Rating.starImage.Equals(this.ratingImage));
}
 
Innerhalb dieser Methode gibt man nun zurück, ob Visual Studio eine Quellcodezeile zum festlegen eines Wertes für diese Eigenschaft erstellen soll. Normalerweise gibt man also false  zurück wenn die Eigenschaft den initialisierten Wert (also Standardwert) aufweist (hier das Standardbild - der Stern), ansonsten true. Im Übrigen wird der Wert der Eigenschaft von Visual Studio verändert, bevor die ShouldSerializeXXX Methode aufgerufen wird.
Zusätzlich kann man eine ResetXXX Methode hinzufügen, womit der Nutzer im Eigenschaften Fenster mittels Menü die entsprechende Eigenschaft wieder auf ihren Standardwert zurücksetzen kann:
 
 
private void ResetRatingImage()
{
this.RatingImage = Rating.starImage;
}
 
Diese Funktionalität können sie in Abbildung 13 sehen. Wenn sie das DefaultValue Attribut verwenden, wird diese Funktionalität automatisch bereitsgestellt.
 
 
Abbildung 13 – Menü zum Zurücksetzen einer Eigenschaften (erreichbar über
 die rechte Maustaste) zum Standardwert
 
 
Ein weiterer Weg um die Initialisierung des Steuerelementes durch Visual Studio zu verbessern ist die Verwendung des ISupportInitialize Interfaces in unserem Steuerelement:
 
public partial class RatingControl : UserControl, ISupportInitialize
{ /* ... */ }
 
Dieses Interface verfügt über zwei Methoden, die wir in unserer Klasse implementieren müssen. Zum einen ist das die BeginInit Methode, die zum Beginn des Initialisierungsvorganges von Visual Studio aufgerufen wird. In dieser Methode setzt man am besten die interne Infrastruktur (hier die RatingConfigChanged Methode) des Steuerelementes mithilfe eines bool Feldes außer Kraft:
 
private bool currentlyInitializing = false;
 
public void BeginInit()
{
currentlyInitializing = true;
}
 
private void RatingConfigChanged(object sender, EventArgs e)
{
if (currentlyInitializing)
return;
           
      //...
}
 
Dadurch wird verhindert, dass diese Methoden zur Aktualisierung des Steuerelementes bei jeder einzelnen Veränderung einer Eigenschaft während der Initialisierung aufgerufen werden. Nachdem die Werte aller Eigenschaften entsprechend konfiguriert sind, ruft Visual Studio die zweite Methode des Interfaces, die EndInit Methode auf. Hier setzen wir unsere bool Variable zurück auf false und rufen entsprechende Methoden auf um das Steuerelement zu aktualisieren:
 
public void EndInit()
{
this.currentlyInitializing = false;
      this.RatingConfigChanged(null, null);     
}
 
Wenn man jetzt das Projekt neu kompiliert und sich erneut die InitializeComponent Methode der Form des Steuerelementes anschaut, kann man sehen, dass Visual Studio jetzt Gebrauch macht vom ISupportInitialize Interface:
 
private void InitializeComponent()
{
      //...
 
((System.ComponentModel.ISupportInitialize)(this.ratingControl1))
.BeginInit();
 
//werte von ratingControl1 werden hier festgelegt...           
            ((System.ComponentModel.ISupportInitialize)(this.ratingControl1))
.EndInit();
 
//...
}
 
Ferner ist das ISupportInitialize Interface auch gut dafür geeignet mithilfe dessen Methoden evtl. vorhandene Überprüfungsmachanismen für Eigenschaften (hier z.B. dass RatingValue nicht größer sein darf als Maximum) temporär zu deaktivieren, um zu verhindern, dass diese Exceptions verursachen wenn sie in der falschen Reihenfolge von Visual Studio initialisiert werden. Das ist hier nicht der Fall, da Visual Studio die Eigenschaften entsprechend der Reihenfolge ihrer Anfangsbuchstaben initialisiert, und günstigerweise kommt M (Maximum) vor R (RatingValue).
 
Vielleicht haben Sie es bereits gemerkt: wenn man die Maximum Eigenschaft des Steuerelementes verändert (wodurch sich dann auch die Größe des Steuerelementes ändert), aktualisiert der Visual Studio Designer nicht sofort die Markierung um das Steuerelement, wie man in Abbildung 14 sehen kann.
 
Abbildung 14 – Markierung des Steuerelementes wird sofort aktualisiert
 
 
Man kann eine sofortige Aktualisierung erreichen, indem man das RefreshPropertiesAttribute für die entsprechende Eigenschaft verwendet:
 
[RefreshProperties( RefreshProperties.All)]
[DefaultValue(5)]
public int Maximum
{ /* ... */ }
 
Als Argument dafür kann man zwischen RefreshProperties.All (alles wird aktualisiert), RefreshProperties.Repaint (das Steuerelement wird neu gezeichnet) und RefreshProperties.None (nichts wird aktualisiert) wählen.
Jetzt wird die Markierung des Steuerelementes auch sofort aktualisiert, wenn man den Maximum Wert ändert. In diesem Beispiel war es nur die Markierung, bei anderen Steuerelementen hingegen kann dieses Attribut wichtiger sein, z.B. damit das Steuerelement entsprechend der Veränderungen die vorgenommen wurden richtig gezeichnet wird.
 
Attribute, Attribute und nochmals Attribute
 
Wie praktisch Attribute sein können, haben wir ja schon häufig genug im Verlaufe dieses White Paper gesehen. In der Tat gibt es noch viele andere Attribute die nützlich sind für die Entwurfszeit, u. a. das Description Attribut. Damit kann man Eigenschaften und Events eine Beschreibung zuordnen:
 
[Description("Image used to display RatingValue")]
public Image RatingImage
{
//... 
}
 
Diese Beschreibung wird dann im Eigenschaften Fenster angezeigt, wie man in Abbildung 15 sehen kann.
 
 Abbildung 15 – Beschreibung der RatingImage Eigenschaft im Eigenschaften Fenster
 
Zusätzlich gibt es zudem das Category Attribut, mit dem man eine Eigenschaft (hier die Rating Eigenschaft des Steuerelementes) einer Kategorie zuordnen kann:
 
[Category("Look and Feel")]
public Rating Rating
{ /* ... */ }
 
 
Entsprechend wird dann die Eigenschaft im Eigenschaften Fenster unter dieser Kategorie angezeigt (Abbildung 16).
 
 Abbildung 16Rating Eigenschaft unter der Look and Feel Kategorie
 
 
Ein weiteres nützliches Attribut ist das DefaultEvent Attribut. Dieses Attribut wird auf das Steuerelement angewendet, mit dem Namen des Standard - Events des Steuerelementes als Argument:
 
[DefaultEvent("ValueChanged")]
[Designer(typeof(RatingControlDesigner))]
public partial class RatingControl : UserControl, ISupportInitialize
{ /* ... */ }
 
Dadurch wird das Event bestimmt, welchem bei Doppelklick auf das Steuerelement zur Laufzeit der passende Delegat hinzugefügt wird. Visual Studio erzeugt dann automatisch eine Methode für diesen Delegaten und sie landen in der Code View in dieser Methode (wie z.B. beim Doppelklick auf ein Button Steuerelement in der entsprechend erzeugten xxxClick Methode).
 
Entsprechend gibt es auch ein DefaultProperty    Attribut:
 
[DefaultProperty("Rating")]
[DefaultEvent("ValueChanged")]
[Designer(typeof(RatingControlDesigner))]
public partial class RatingControl : UserControl, ISupportInitialize
{ /* ... */ }
 
Dieses Attribut bestimmt welche Eigenschaft des Steuerelements im Eigenschaften Fenster markiert ist (hier die Rating Eigenschaft) wenn sie eine neue Instanz des Steuerelementes zur Form hinzufügen (Abbildung 17).
 
 Abbildung 17Rating Eigenschaft ist markiert im Eigenschaften Fenster
 
 
Andere nützliche Attribute umfassen das Browsable Attribut, das mittels eines boolschen Arguments bestimmt, ob eine Eigenschaft oder ein Event eines Steuerelements im Eigenschaften Fenster angezeigt wird, z.B. so:
 
[Browsable(false)]
public int RatingValue
{ /* ... */ }
 
 
Das Beispiel oben macht natürlich für unser Steuerelement hier keinen Sinn.
Ferner gibt es das Localizable Attribut für Eigenschaften. Dieses legt fest, ob bei Lokalisierung der Wert der Eigenschaft von Visual Studio in Resourcen Dateien entsprechend ausgelagert werden soll (z.B. kann das bei einer Text Eigenschaft Sinn machen). Die Anwendung des Attributes könnte so aussehen:
 
[Localizable(true)]
public int Maximum
{ /* ... */ }
 
 
Auch das macht bei unserem Steuerelement hier keinen Sinn. Dennoch ist das Localizable Attribut im Allgemeinen aber definitiv ein sehr nützliches Hilfsmittel.
Zusätzlich gibt es es noch das DesignerSerializationVisibility Attribut, mit dem komplett kontrolliert werden kann, wie und ob der Wert einer Eigenschaft eines Steuerelementes von Visual Studio entsprechend als Quellcodezeile in die InitializeComponent Methode serialisiert wird. Dieses Attribut kann verwendet werden, um noch mehr Kontrolle über den Code - Serialisierungsvorgang von Visual Studio zu erreichen - mehr als mittels DefaultValue Attribut und der ShouldSerializeXXX Methode, die bereits im 5. Abschnitt „Optimierungen“ beschrieben wurden. Für dieses Attribut gibt es 3 mögliche Argumente:   DesignerSerializationVisibility.Hidden gibt an, dass von Visual Studio keine Quellcodezeile in der InitializeComponent Methode für die Eigenschaft erzeugt wird, um den Wert entsprechend zuzuweisen. DesignerSerializationVisibility.Visible gibt an, dass eine Quellcodezeile erzeugt wird. DesignerSerializationVisibility.Content gibt an, dass Visual Studio Code für den Inhalt des Objektes der Eigenschaft erzeugt, anstatt für das Objekt selbst. Die Anwendung des Attributes könnte dann z.B. so aussehen:
 
 
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[Category("Look and Feel")]    
public Rating Rating
{ /* ... */ }
 
Zusammenfassung
 

In diesem White Paper wurde einiges an Möglichkeiten beschrieben, um Steuerelementen nützliche Unterstützung für die Entwurfszeit hinzuzufügen. Je nach Umfang und Art des Steuerelementes kann die eine oder andere Art von Entwurfszeit – Funktionalität nützlich sein. Je umfangreicher die Steuerelemente aufgebaut sind, desto zeitintensiver wird dessen Konfiguration – ein Umstand den eine sinnvolle Unterstützung für die Entwurfszeit deutlich abmildern kann. Das Beispielprojekt dieses Blog Eintrages finden Sie unter: RatingControlProjekt

veröffentlicht am 16.04.2007 13:18

Kommentare

Bisher kein Feedback.

Kommentar abgeben

Titel:
Name:
Email:
(wird nicht angezeigt!)
Homepage:
Feedback:
Please add 2 and 3 and type the answer here: