Sebastians Weblog

ESCde Developer Blog

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

Archiv

Post Kategorien

ESCde

ESCde Blogger

Freitag, 13. Februar 2009 #

Schon einmal ein Problem bei der Team Foundation Server Installation gehabt? Unter [1] finden Sie wertvolle Lösungshinweise.
[1] http://msdn.microsoft.com/de-de/library/dd266793(en-us).aspx

Montag, 16. April 2007 #

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


Unter folgendem Link finden Sie diesen Blog Eintrag als Word Dokument: CSharp3Whitepaper

Einleitung

 

Dieses Whitepaper beschäftigt sich mit den Erweiterungen und Veränderungen von C# 3.0. C# 3.0 wird mit dem .NET Framework 3.5 (zusammen mit Visual Studio Codename Orcas) voraussichtlich Mitte 2007 erscheinen. Beachten Sie, dass C# 3.0 nicht Teil des .NET Framework 3.0 ist. Es mag von den Versionsnummern her etwas verwirrend sein, aber das .NET Framework 3.0 enthält weder Compilerupdates noch eine veränderte CLR, sondern nur neue Bibliotheken und wurde aus rein Marktstrategischen Gründen seitens Microsoft von WinFX in .NET 3.0 umbenannt. Die Erweiterungen in C# 3.0 reichen von kleinen Veränderungen wie z.B. implizit typisierten lokalen Variablen bis hin zu ziemlich neuen Funktionen wie z.B. Erweiterungsmethoden. Sogar revolutionäre Neuerungen wie etwa die normalerweise aus der Datenbank -Welt bekannten Query Expressions sind in C# 3.0 zu finden (diese werden als LINQ bezeichnet).

 

Implizit typisierte lokale Variablen

 

In C# 3.0 kann der Datentyp lokaler Variablen bei der Deklaration vom Compiler automatisch, also ohne dass explizit der Typ angegeben wird, bestimmt werden. Dafür wird das neue Schlüsselwort var verwendet. Ein Beispiel:

 

var deepThought = 42;

 

Der Compiler wertet hier dann die Initialisierung der Variable deepThought aus, und legt aufgrund des Wertes 42 den Typ von deepThought auf Integer fest. Dies bedeutet, dass die obige Zeile semantisch identisch mit folgendem ist:

 

int deepThought = 42;

 

Der Datentyp wird einmalig bei der ersten Deklaration der Variablen vom Compiler festgelegt, und kann danach nicht mehr verändert werden:

 

var i = 5; // i wird vom Compiler als Integer festgelegt

 

i = 25.2;  //Fehler, da i Integer ist, und kein Double

 

Damit der Compiler den Typ der Variable feststellen kann, muss eine mit dem Schlüsselwort var deklarierte Variable unbedingt bei der Deklaration schon initialisiert werden, da aus der Intitialisierung der Datentyp der Variable vom Compiler abgeleitet wird. Daraus ergibt sich auch, dass eine Initialisierung mit null in diesem Zusammenhang nicht zulässig ist. Weitere Beispiele sind:

 

var fehler   = null;                 //Fehler -> ist nicht erlaubt

var fehler2;                         //Fehler -> Initialiserung fehlt

     

var liste    = new List<int>();      //Typ: List<int>

var text     = "Das ist ein Text";   //Typ : String

var pi       = 3.1415;               //Typ double

var arrayVar = new int[42];          //Typ: int[]

 

foreach (var myInt in arrayVar)      //Typ von myInt: int

{

Console.WriteLine(myInt);

}

 

Auch diese Beispiele sind natürlich wieder semantisch äquivalent zur „herkömmlichen“ Deklaration wo der Datentyp explizit angegeben wird.

Wie man in späteren Kapiteln dieses Whitepapers noch sehen wird, erweist sich das var Schlüsselwort vor allem für Anonyme Typen und Linq als notwendig bzw. praktisch.

Erweiterungsmethoden

 

Erweiterungsmethoden stellen eine interessante neue Möglichkeit dar, um Klassen oder auch Strukturen zu ergänzen. Dies umfasst auch Klassen und Strukturen, bei denen es bisher nicht möglich war, so z.B. Klassen die sealed sind oder Strukturen wie System.Int32. Erweiterungsmethoden werden als statische Methode in einer neuen static Klasse implementiert, und können dann wie eine normale Methode (d.h. Instanzmethode) des erweiterten Datentyps aufgerufen werden. Um eine Methode als Erweiterungsmethode zu deklarieren, wird vor dem ersten Parameter das Schlüsselwort this angegeben. Ferner gibt der Argumenttyp des ersten Parameters die zu erweiternde Klasse bzw. Struktur an. Wenn die Erweiterungsmethode dann aufgerufen wird,  übergibt der Compiler die Instanz des erweiterten Typs als erstes Argument an die Methode. Erweiterungsmethoden können z.B. so aussehen:

 

public static class Extension

{    

//Erweiterungsmethode für Int

public static int Betrag(this int instanz)

      {

            if (instanz < 0)

            {

                return -1 * instanz;

            }

 

            return instanz;

      }

 

// 2. Erweiterungsmethode, auch für Int

      public static int Faktor(this int instanz, int faktor)

      {

            return instanz * faktor;

      }

}

 

int testZahl = -42;

 

Console.WriteLine(testZahl.Betrag().ToString());     // Ausgabe: 42

 

Console.WriteLine(testZahl.Faktor(5).ToString());    // Ausgabe: -210

 

 

Das Beispiel gibt 42 und – 210 aus. In diesem Beispiel kann man nun die Betrag und Faktor Methoden für jede Integer Variable nutzen, als wären die Methoden direkt in der Basisklasse System.Int32 als Instanzmethoden vorhanden. Wenn in der System.Int32 Struktur bereits eine Betrag-Methode mit der gleichen Signatur wie die gleichnamige Erweiterungsmethode existieren würde, so hätte die bereits vorhandene Methode in System.Int32 Vorrang vor der Erweiterungsmethode. Zudem muss auch die Einordnung der Erweiterungsmethode bezüglich des Namensraumes beachtet werden, da der Compiler die erste Erweiterungsmethode nimmt die (hinsichtlich der Namensräume) erreichbar ist.

 

 

Ein weiterer interessanter Aspekt ist, dass Erweiterungsmethoden auch vererbt werden, wie im folgenden Beispiel sichtbar ist:

 

 

public class BaseClass { }

 

public class InheritedClass : BaseClass { }

 

public static class Extension

{    

//erweitert BaseClass

public static string WriteType(this BaseClass b)

      {

            return b.GetType().ToString();

      }

}

 

 

InheritedClass inhClass = new InheritedClass();

 

Console.WriteLine(inhClass.WriteType());  //gibt "Test.InheritedClass" aus

 

 

Die Erweiterungsmethode WriteType erweitert eigentlich die Klasse BaseClass. Man kann aber WriteType nun auch als Instanzmethode aller Klassen nutzen, die von BaseClass abgeleitet sind. In dem Beispiel wird WriteType also wie eine Instanzmethode von InheritedClass behandelt, da InheritedClass von BaseClass abgeleitet ist.

Objekt Initialisierer

 

Durch Objekt Initialisierer wird es in C# 3.0 möglich, ähnlich der Initialisierung von Attributen elegant Felder und Eigenschaften einer Klasse oder Struktur zu initialisieren. Somit können nun öffentliche Eigenschaften und Felder von Objekten ohne das explizite Vorhandensein des jeweiligen Konstruktors in beliebiger Reihenfolge initialisiert werden. Dies geschieht über geschweifte Klammern, in denen die einzelnen Felder oder Eigenschaften des Objektes mit Werten belegt werden:

 

 

public class Adresse

{

public string Name;

      public string Strasse;

      public int PLZ;

      public string Ort;

}

 

Adresse adrEsc = new Adresse { Name = "ESC Deutschland", Strasse =

 "Am Fasanengarten 5", PLZ = 76131,

 Ort = "Karlsruhe" };

 

Ein weiteres Beispiel mit Schachtelung:

 

Rectangle rect = new Rectangle { Location = new Point { X = 7, Y = 3 },

    Size = new Size { Width = 21,

    Height = 42 }

  };

 

 

Im Beispiel oben hätte man das gleiche einfach über entsprechend verfügbare Konstruktoren erreichen können, aber häufig sind solche umfangreichen Konstruktoren einfach nicht vorhanden. Diese Art von Konstruktoren sind in C# 3.0 nun aber auch nicht mehr notwendig.

Collection Initialisierer

 

Mithilfe von Collection Initialisierern kann man bei der Initialisierung eines Objektes, das ICollection<T> (z.B. List<T>) implementiert, elegant Elemente hinzufügen. Man verwendet geschweifte Klammern, in denen die einzelnen Elemente mit Kommata voneinander getrennt sind – was dann zum Beispiel so aussieht:

 

List<int> intList = new List<int>() { 0, 1, 1, 2, 3, 5, 8, 13, 21, 34 };

 

Für die angegebenen Elemente wird entsprechend ihrer Reihenfolge die Methode

ICollection<T>.Add(T element)  aufgerufen. Natürlich müssen die aufgelisteten Elemente vom Typ T sein oder es muss eine implizite Konvertierung zu T existieren, wie in den folgenden zwei Beispielen deutlich wird:

 

 

//Fehler, double kann nicht (implizit) in int konvertiert werden

List<int> intList = new List<int>() { 0, 1, 1, 2, 3.58};

 

//OK, da implizite Konvertierungen von float zu double und von int zu //double existieren

List<double> intList = new List<double>() { 0, 1, 1, 2, 3F};

Anonyme Typen

 

Anonyme Typen sind vom Compiler automatisch erzeugte simple, namenlose Klassen, die nur über readonly Eigenschaften (also nur mit get) und dazugehörige private Felder verfügen. Mit namenlos ist gemeint, dass der Compiler der Klasse einen uns nicht bekannten Namen gibt, so dass man nicht direkt Zugriff auf die Klasse hat. Stattdessen erhält man lediglich eine Instanz die man ausschließlich lokal (im Bereich der Deklaration) verwenden kann.

Anonyme Typen werden mittels eines anonymen Objekt Initialisierers deklariert, also ein Objekt Initialisierer bei dem man den Klassennamen weglässt. Durch diesen anonymen Objekt Initialisierer wird dann die Klasse mit den entsprechenden Eigenschaften erzeugt und man erhält eine Instanz. Anhand der Eigenschaften im Objekt Initialisierer und des jeweiligen Typs der zugewiesenen Werte erzeugt der Compiler die anonyme Klasse, was dann etwa wie folgt aussehen kann:

 

 

var person = new { Vorname = "Bill", Nachname = "Gates", Alter = 56};

 

Hierfür generiert der Compiler intern (in MSIL) die folgende Klasse:

 

    internal class ??????

    {

        private string _vorname;

        private string _nachname;

        private int _alter;

 

        public string Vorname

        {

            get { return _vorname; }

        }

 

 

        public string Nachname

        {

            get { return _nachname; }

        }

 

 

        public int Alter

        {

            get { return _alter; }

        }

    }

 

 

Die vom Compiler generierte Klasse enthält wirklich nur readonly Eigenschaften und entsprechende  Felder, wie auch oben im Beispiel dargestellt.

Da der Typ der Eigenschaften aus der jeweiligen Klasse bzw. Struktur des zugewiesenen Wertes im Objekt Initialisierer abgeleitet wird, darf man hier nicht null zuweisen – ansonsten  kann der Compiler den Datentyp der Eigenschaft nicht bestimmen:

 

 

//Fehler -> Compiler kann Typ von Eigenschaft „Taktfrequenz“ nicht ableiten

var pc = new { CPU = "Intel P4" , Taktfrequenz = null, Ram = 2048};

 

 

Sobald eine weitere anonyme Klasse deklariert wird, bei der im Objekt Initialisierer Eigenschaften mit dem gleichen Namen, Typ und in der gleichen Reihenfolge wie

bei einer anderen bereits vorhandenen anonymen Klassen angegeben sind, verwendet der Compiler die gleiche anonyme Klasse und es sind untereinander Zuweisungen möglich:

 

 

var pc = new { CPU = "Intel P4" , Cores = 1,

   Taktfrequenz = 2.66, Ram = 2048};

 

var schnellererPC = new { CPU = "Intel Core 2 Duo Extreme", Cores = 2,

   Taktfrequenz = 6.00, Ram = 4096};

 

pc = schnellererPC;

 

 

Wie man oben sehen kann, sind der Name, der Typ und die Reihenfolge der Eigenschaften im Objekt Initialisierer bei pc und schnellererPC identisch, und somit ist schnellererPC vom gleichen Typ wie pc. Diese Tatsache erlaubt es uns in der 3. Zeile des Beispiels dass wir die beiden Variablen untereinander zuweisen können.

 

 

Lambda Expressions

 

Lambda Expressions sind funktional erweiterte anonyme Methoden (anonyme Methoden gibt es seit C# 2.0). Der augenscheinlichste Unterschied zu anonymen Methoden ist die Syntax: auf die Parameterliste folgt ein Doppelpfeil (=>), gefolgt von einer einzelnen Anweisung oder einem Anweisungsblock, z.B.:

 

(int x, int y) => x + y  //nur eine Anweisung

 

oder:

 

//mit Anweisungsblock

(int x) => { if( x < 0) 

                  x = -1 * x;

     

             return x;

           }

 

 

Der Typ des Ausdruckes hinter dem Doppelpfeil bestimmt den Rückgabetyp der Lambda Expression. Dementsprechend hat eine Lambda Expression mit nur einem Methodenaufruf nach dem Doppelpfeil den gleichen Rückgabetyp wie diese Methode. Falls dem Doppelpfeil ein Anweisungsblock folgt, so wird der Rückgabetyp durch den Ausdruck nach dem return Schlüsselwort festgelegt. Sollte das return Schlüsselwort im Anweisungsblock fehlen, dann ist der Rückgabetyp der Lambda Expression void. In folgenden Beispielen wird die Bestimmung des Rückgabetyps veranschaulicht:

 

 

//Rückgabetyp ist void, da Console.WriteLine() void als Rückgabetyp hat

(int i) => Console.WriteLine(i.ToString());

 

 

//Rückgabetyp ist Int, da x und y vom Typ Int sind

(int x, int y) => x + y; 

 

 

//Rückgabetyp ist void, da kein return Schlüsselwort im Block nach //Doppelpfeil

(double x) => { x = 3.14;

                MessageBox.Show(x.ToString());}

 

 

//Rückgabetyp ist double, da ein double (0.5) zu einem

//int addiert wird und das Ergebnis ein double ist

(int i) => i + 0.5;

 

 

//Rückgabetyp ist string, da nach Doppelpfeil

//ein string zusammengesetzt wird

(int alter) => "Glückwunsch zum " + alter + ". !"

 

 

Genau wie bei anonymen Methoden auch muss natürlich die Anzahl der Parameter und deren jeweiliger Datentyp mit denen des Delegate, für den die Lambda Expression angegeben wird, übereinstimmen. Allerdings kann bei Lambda Expressions auf die Angabe des Typs für die Parameter verzichtet werden, da diese vom Kontext her abgeleitet werden können. Dem jeweiligen Parameter der Lambda Expression wird also automatisch der Typ des entsprechenden Parameters des Delegate, für den die Lambda Expression angegeben wird, zugewiesen. In folgendem Beispiel kann man dieses Konzept sehen:

 

public class Person

{

public string Name = "";

public int Alter = 0;

}

 

public delegate void TestDelegate(Person person);

 

//wegen TestDelegate ist der Parameter x vom Typ Person

//Rückgabetyp muss void sein, auch durch TestDelegate vorgegeben

TestDelegate del = ( x ) => Console.WriteLine(x.Name);

 

//nun darf man bei nur einem Parameter auch die Klammern weglassen

TestDelegate del2 = x => Console.WriteLine(x.Name);

 

Gleichermaßen wie bei den Parametern muss natürlich auch der Rückgabetyp der Lambda Expression mit dem Rückgabetypen des Delegate übereinstimmen (bzw. eine implizite Konvertierung existieren). Demgemäß liefert die Lambda Expression im obigen Beispiel keinen Rückgabewert, da void vom Delegate TestDelegate entsprechend als „Rückgabewert“ vorgegeben wird. Ein weiteres Beispiel hierfür:

 

public delegate int AnotherTestDelegate();

 

AnotherTestDelegate del = () => 0.5; //Fehler

 

Dieses Beispiel produziert einen Compiler-Fehler, weil durch den Delegate AnotherTestDelegate ein Integer als Rückgabewert erwartet wird, die Lambda Expression aber einen double zurückgibt, der auch nicht implizit zu einem Integer konvertiert werden kann.  Man beachte die leeren Klammern vor dem Doppelpfeil in der Lambda Expression. Diese bedeuten, dass die Lambda Expression über keinerlei Eingabeparameter verfügt, gemäß der Definition des Delegate AnotherTestDelegate.

Im Gegensatz zu anonymen Methoden unterliegen Lambda Expressions auch den Regeln des Methoden - Überladens und des Ableitens von Parametertypen für Generics. Nehmen wir etwa folgendes Beispiel:

 

delegate double TestDel<T>(T a);

 

private static void DoSimpleMath(TestDel<int> testDel)

{

Console.WriteLine("int");

}

 

private static void DoSimpleMath(TestDel<double> testDel)

{

Console.WriteLine("double");

}

 

DoSimpleMath((double x) => x + 0.5);

 

 

Das obige Beispiel gibt „double“ aus, da der Datentyp des Parameters für die Lambda Expression double ist (siehe gelbe Markierung). Somit wird für den generischen  Platzhalter T von TestDel (dem Argument -Typ der beiden DoSimpleMath Methoden) double als Typ eingesetzt und damit dementsprechend die DoSimpleMath(TestDel<double> testDel) Methode aufgerufen. Gleichermaßen wird auch für den Rückgabetypen der Lambda Expressions der „passendste“ generische Delegate und damit die entsprechend passende überladene Methode aufgerufen:

 

delegate T TestDel<T>();

 

private static void DoSimpleMath(TestDel<int> testDel)

{

Console.WriteLine("int");

}

 

private static void DoSimpleMath(TestDel<double> testDel)

{

Console.WriteLine("double");

}

 

DoSimpleMath(() => 5);

 

 

Dieses Beispiel gibt nun „int“ aus, da die Lambda Expression einen int (siehe gelbe Markierung) zurückgibt und somit der (Rückgabe -) Platzhalter T vom Delegate TestDel auf int festgelegt wird. In Folge dessen wird die DoSimpleMath(TestDel<int> testDel) Methode verwendet. Für die Zeile

 

DoSimpleMath(() => 42.0);

 

wird analog entsprechend „double“ ausgegeben, also die DoSimpleMath(TestDel<double> testDel) Methode aufgerufen, da nun die Lambda Expression einen double zurückgibt (siehe gelbe Markierung).

LINQ

 

LINQ (Language Integrated Query) ist die größte Neuerung in C# 3.0. Wie der Name schon besagt, handelt es sich dabei um in die Sprache integrierte, SQL ähnliche Strings, mit denen fast jede Art von Daten vergleichsweise elegant gefiltert, gruppiert und sortiert werden kann. Nehmen wir folgendes einfaches Beispiel:

 

int[] integers = new int[] { 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55};

 

IEnumerable<int> evenNumbers =

from myNumber in integers where myNumber % 2 == 0 select myNumber;

 

foreach (int number in evenNumbers)

{

Console.WriteLine(number.ToString());

}

 

Dieses Beispiel gibt 0, 2, 8 und 34 aus, also alle geraden Zahlen des Arrays integer. In der 2. Zeile des Beispiels kann man dann auch die eigentliche LINQ Expression sehen:

 

from myNumber in integers where myNumber % 2 == 0 select myNumber

 

Wie alle LINQ Ausdrücke in C# fängt auch dieser mit dem from  Schlüsselwort an. Es folgt eine Laufvariable (myNumber), die ähnlich funktioniert wie die Laufvariable in einer foreach Schleife:

 

foreach (int myNumber in integers)

{  }

 

Die Quellmenge des LINQ Ausdruckes (im Beispiel das Array integers) wird ähnlich einer Schleife „durchlaufen“ und jedes einzelne Elemente dann jeweils durch diese Laufvariable repräsentiert. Diese Laufvariable ist die eigentliche Variable auf die innerhalb des LINQ Ausdruckes sämtliche Operatoren (z.B. Filterkriterien) angewandt werden. Nach dem in Schlüsselwort kommt die zu filternde Menge (hier integers), gefolgt von dem where Operator mit dem eigentlichen Filterausdruck als Lambda Expression:

 

myNumber % 2 == 0  //wähle alle geraden Zahlen

 

Der where Operator hat die gleiche Funktion wie der gleichnamige Operator in SQL, sprich das Filtern. Dieser Filterausdruck wird nun auf jedes einzelne Element vom Array integers angewandt. Über den select Operator gibt man an, was genau vom Element zurückgegeben werden soll, wie im nächsten Beispiel besser sichtbar wird:

 

public class Person

{

public string Name = "";

public int Alter = 0;

}

 

 

 

 

Person[] persons = new Person[] {new Person {Name = "Arthur", Alter = 35},

    new Person { Name = "Marvin", Alter = 40},   

    new Person { Name = "Ford", Alter = 55} };

 

IEnumerable<string> filteredPersons =

from person in persons where person.Alter < 50 select person.Name;

 

 

foreach (string name in filteredPersons)

{    

//gibt die Namen der Personen mit Alter < 50 aus

Console.WriteLine(name);

}

 

In diesem Beispiel wird nun jeweils nur der Name der Personen, die dem Filterkriterium (Alter < 50) entsprechen, zurückgegeben – gemäß der Auswahl mit dem select Operator (person.Name).

In beiden Beispielen oben kann man sehen, dass der LINQ Ausdruck eine Instanz vom Typ IEnumerable<T> zurückgibt. Dies ist aber nicht immer so, da der Rückgabetyp von den benutzten LINQ -Operatoren abhängt.  So gibt es z.B. den Count Operator, der die Anzahl der Elemente, die zu einem Filterausdruck oder Ähnlichem passen, als Integer zurückgibt:

 

 

int[] integers = new int[] {0,1,3,5,7,9,12,42, 99, 200, 300 };

 

int count = (from number in integers where number < 42 select

number).Count();

 

Console.WriteLine(count.ToString()); //Ausgabe: 7

 

 

Hier wird nun mit Hilfe des Count Operators die Anzahl der Zahlen ausgegeben, die kleiner als 42 sind. Vielleicht fragen Sie sich jetzt, warum der Count Operator wie eine Methode aufgerufen wird. Das hängt damit zusammen, dass eigentlich alle LINQ Operatoren als Methoden implementiert sind. Genauer gesagt sind sie als Erweiterungsmethoden in der System.Query.Sequence Klasse enthalten und erweitern alle Typen die IEnumerable<T> implementiert haben - also unter anderem sämtliche Arrays, List<T> und Stack<T> sowie viele mehr. Daher können LINQ Expressions auf alle diese Typen angewandt werden. Praktischerweise kann man dadurch auch die komplexeren Klassen im Array wie Daten aus Datenbanken behandeln und entsprechend mit LINQ elegant filtern, sortieren und gruppieren usw. Hierfür gibt es dann auch dutzende Operatoren (eine kompakte Übersicht mit Beispielen finden Sie unter  http://msdn2.microsoft.com/en-us/vcsharp/aa336746.aspx), von denen einige auch von Datenbankabfrage -Sprachen wie SQL her bekannt sind und ähnlich funktionieren - so z.B. die Operatoren Count, Average und GroupBy. Da wie bereits beschrieben die Operatoren als Methoden realisiert sind, können sie auch ganz normal als solche entsprechend aufgerufen werden. Die Syntax, die Sie in den ersten beiden LINQ Beispielen gesehen haben, ist also nichts weiter als „Syntaktischer Zucker“ - der C# Compiler ruft einfach für die Schlüsselwörter from, where und select etc. die dazu passenden LINQ Methoden auf. Somit sind auch die beiden folgenden LINQ Ausdrücke semantisch äquivalent:

 

 

//Linq Ausdruck mit C# Schlüsselwörtern

IEnumerable<int> numbersSmallerThan42 = from number in integers where

number < 42 select number + 10;

 

 

//der gleiche Linq Ausdruck wie oben, aber durch Methodenaufrufe der //entsprechenden Linq -Operatoren (wird Punkt -Notation genannt)

IEnumerable<int> againNumbersSmallerThan42 = integers.Where(i =>

i < 42).Select(i => i + 10);

 

 

Das Beispiel gibt alle Zahlen aus dem Array integers zurück die kleiner 42 sind und addiert dann jeweils 10. Welche der beiden Schreibweisen (C# Schlüsselwörter oder Punkt -Notation) Sie bevorzugen und nutzen bleibt somit ganz Ihnen überlassen.

 

Ein weiterer wichtiger Aspekt bei LINQ ist der Zeitpunkt, zu dem der LINQ Ausdruck ausgeführt wird. Die LINQ Expression wird aufgrund ihrer internen Implementierung (mittels yield) nicht an der Stelle wo sie deklariert ist ausgeführt. Stattdessen kommt sie erst zum Zuge, wenn auf die Elemente der Rückgabeinstanz des LINQ Ausdruckes zugegriffen wird. Dadurch kann man vor der eigentlichen Verwendung der Rückgabe -Elemente des LINQ Ausdruckes noch das zu filternde Objekt verändern. Diese Veränderungen werden von dem LINQ Ausdruck dann auch berücksichtigt:

 

 

int[] integers = new int[8];

 

for (int i = 0; i < 5; i++) //initialisiert integers mit den Zahlen 1 - 5

{

integers[i] = i + 1;

}

 

          

//liefert alle Zahlen aus integers die kleiner 0 sind, also würde //normalerweise keine einzige Zahl zurückgegeben werden

IEnumerable<int> numbers = from number in integers

where number < 0 select number;

 

 

integers[5] = -42;

integers[6] = -7;

integers[7] = -9;

 

foreach (int myNumber in numbers)

{

//gibt - 42, -7 und -9 aus, da jetzt erst auf Elemente von numbers //zugegriffen wird, und damit erst HIER der LINQ Ausdruck über das //integers – Array läuft

Console.WriteLine(myNumber.ToString());

}

 

 

Der LINQ Ausdruck wird in diesem Beispiel innerhalb der foreach Schleife ausgeführt,

da erst hier der Zugriff auf die Elemente von numbers (dem Rückgabewert der LINQ Expression) erfolgt. Folglich gibt das Beispiel die kurz vorher zu integers hinzugefügten negativen Zahlen aus, obwohl es an der eigentlichen Stelle der Deklaration des LINQ Ausdruckes in integers noch keine negativen Zahlen gibt.

Dieses Verhalten von LINQ tritt jedoch nicht immer auf. Bei Verwendung der Aggregat -Operatoren (z.B. Count, Min, Max und Average) wird der LINQ Ausdruck sofort an der Stelle ausgeführt wo er deklariert ist, wie man etwa in folgendem Beispiel sehen kann:

 

 

int[] integers = new int[8];

 

for (int i = 0; i < 5; i++) //initialisiert integers mit den Zahlen 1 - 5

{

integers[i] = i + 1;

}

 

//LINQ Ausdruck wird wegen Count Operator sofort hier ausgeführt

int numbersCount = (from number in integers

where number < 0 select number).Count();

 

integers[5] = -42;

integers[6] = -7;

integers[7] = -9;

 

//gibt 0 aus, da der LINQ Ausdruck oben bei der Deklaration ausgeführt //wurde, und zu diesem Zeitpunkt gab es im integers Array keine Zahlen < 0

Console.WriteLine(numbersCount.ToString());

 

 

Das Beispiel oben gibt diesmal 0 aus, da der LINQ Ausdruck aufgrund des Count Operators direkt an der deklarierten Stelle ausgeführt wird. Somit haben die späteren Änderungen von integers keinerlei Auswirkung auf das Ergebnis der LINQ Expression.

 

Zusätzlich zu den oben beschriebenen Features von LINQ wird es LINQ auch für XML betreffende Klassen (Bezeichnung: XLINQ) sowie für Datenobjekte von ADO.NET (Bezeichnung: DLINQ) geben.

Schlusswort

 

Wie Sie in diesem Whitepaper lesen konnten, wird C# 3.0 mit einigen interessanten Neuerungen aufwarten, Features, die C# noch produktiver machen werden. Dadurch wird die Programmierung in einigen Anwendungsgebieten noch weiter vereinfacht werden – vor allem durch LINQ, das die bisher vorhandene Lücke zwischen der Anwendungsprogammierung und Datenbanken (bzw. Datenbanksprachen) verkleinern wird. Um die Beispiele nachvollziehen zu können oder auch um einfach mit C# 3.0 zu experimentieren brauchen sie das LINQ May 2006 CTP (zu finden unter:

http://www.microsoft.com/downloads/details.aspx?FamilyID=1E902C21-340C-4D13-9F04-70EB5E3DCEEA&displaylang=en  ).