Share via


Dr. GUI .NET 1.1 #3

 

17. Juni 2003

Zusammenfassung: Dr. GUI zeigt das kanonische Polymorphismusbeispiel, eine Anwendung, die ziehbare Objekte von einer allgemeinen abstrakten Basisklasse ableiten. Außerdem veranschaulicht er die Verwendung einer Schnittstelle und einige Entwurfsmuster, um allgemeinen Code bei der Implementierung einer Schnittstelle oder abstrakten Basisklasse zu berücksichtigen. Schließlich veranschaulicht er eine einfache Windows Forms-Anwendung und eine ASP.NET Version der Zeichnungsanwendung, die eine Bitmap auf der Serverseite ohne Unterstützung durch clientseitige Skripts oder anderen Clientcode bearbeitet. (42 gedruckte Seiten)

Dr. GUI .NET-Startseite
Quellcode für diesen Artikel
Führen Sie die Beispielanwendung GraphicalASPX aus.

Inhalte

Einführung
Wo wir waren; Wohin wir gehen
Alles zusammensetzen: Die Zeichenklassen
Verwenden unserer zeichnenden Objekte in einer Windows Forms-Anwendung
Verwenden unserer zeichnenden Objekte in einer ASP.NET-Anwendung
Übergeben des Zustands zwischen Seiten und Anforderungen
Zeichnen in einer separaten Seite
Versuch es doch mal!
Yo! Lassen Sie uns reden!
Was wir getan haben; Was kommt als nächstes

Sprechen Sie aus!

Teilen Sie der Welt und uns mit, was Sie über diesen Artikel auf dem Dr. GUI .NET Message Board denken. Oder lesen Sie, was Dr. GUI denkt – und kommentieren Sie es in seinem Blog.

Sehen Sie sich das Beispiel an, das als Microsoft® ASP.NET-Anwendung ausgeführt wird. Oder werfen Sie einfach einen Blick auf den Quellcode in einem neuen Fenster.

Einführung

Willkommen zurück zum vierten Artikel zur Programmierung von Microsoft .NET Framework von Dr®. GUI. Die ersten drei werden – zu Ehren .NET Framework Arrayelementnummerierung, die bei 0 statt 1 beginnt – Dr. GUI .NET 1.1 #0 bis Dr. GUI .NET 1.1 #2 genannt. Wenn Sie nach den vorherigen Artikeln suchen, sehen Sie sich die Dr. GUI .NET-Startseite an.

Auch hier hofft Dr. GUI, dass Sie am Message Board teilnehmen. Es gibt bereits einige gute Diskussionen, aber sie werden besser sein, wenn Sie mitmachen! Jeder kann die Nachrichten lesen, aber Sie benötigen einen Microsoft .NET Passport, um Sie zu authentifizieren, wenn Sie posten möchten. Aber keine Sorge – es ist einfach, einen Passport einzurichten, und Sie können ihn jedem Ihrer E-Mail-Konten zuordnen.

Wo wir waren; Wohin wir gehen

Beim letzten Mal haben wir die Vererbung und Polymorphie überprüft und gezeigt, wie die .NET Framework sie implementiert.

Dieses Mal sehen wir ein vollständiges Beispiel für Vererbung, abstrakte Basisklassen (MustInherit) und Schnittstellen in einer niedlichen Zeichnungsanwendung. Hierbei handelt es sich nicht um eine Konsolenanwendung. da es grafisch ist, handelt es sich stattdessen um eine Microsoft® Windows® Forms-Anwendung. (Das gibt uns die Möglichkeit, Windows Forms ein wenig zu erkunden.)

Die ASP.NET-Version veranschaulicht die Verwendung benutzerdefinierter Bitmaps auf Webseiten – etwas, das in den meisten Webprogrammiersystemen sehr schwierig ist, aber mit ASP.NET einfach ist. Dr. GUI glaubt, dass Es Ihnen gefallen wird. Führen Sie die Anwendunghttp://drgui.coldrooster.com/drguidotnet/3/GraphicalASPX aus, um sie auszuprobieren.

Alles zusammensetzen: Die Zeichenklassen

Letztes Mal haben wir die Verwendung einiger ziemlich abstrakter Beispiele, Vererbung, Schnittstellen und Polymorphie diskutiert. Der gute Arzt dachte, dass wir dieses Mal etwas anderes machen würden, und werfen einen Blick auf eine kleine, aber etwas realistische Stichprobe, die alles zeigt, was wir gelernt haben. Und da die Anwendung wirklich Grafiken benötigt, gibt dies uns die Möglichkeit, einige neue Dinge zu erkunden: Windows Forms sowie eine sehr coole Technik zum erstellen von Bitmaps im Fliegen zur Anzeige auf Webseiten – alles ohne clientseitige Skripte!

Kanonisches Polymorphismusbeispiel

In der Unterrichtsprogrammierung gibt es eine Reihe von ziemlich standard-Beispielprogrammen, die häufig verwendet werden. Als Dr. GUI anfing, schwor er, dass er niemals eine davon verwenden würde: Dass er niemals eine Zeichenfolgenklasse als Beispiel verwenden würde, weder komplexe Zahlen noch eine Zeichnungsanwendung. Dies wäre schließlich nicht originell.

Nun, wie sich herausstellt, gibt es einen guten Grund (neben Faultier), dass diese Beispiele verwendet werden: Sie sind sehr reichhaltig, sehr leicht zu erklären und zu verstehen, und ungefähr so einfach wie möglich, während sie die entscheidenden Konzepte klar demonstrieren.

Vor langer Zeit kapitulierte der gute Arzt und begann, die "kanonischen" Beispiele zu verwenden. Daher haben wir für diese Spalte den Enkel der Polymorphismusbeispiele: das polymorphe Zeichenprogramm.

Hier sehen Sie einen Screenshot, der zeigt, wie die Windows Forms Version dieses Programms aussieht:

Abbildung 1. Windows Form-Version des kanonischen Polymorphismusbeispiels

Und so sieht die ASP.NET-Version im Browser aus:

Abbildung 2. ASP.NET Version des kanonischen Polymorphismusbeispiels

Sie können die oben gezeigte ASP.NET Version ausführen .

Was wir hier tun

Die Grundidee dieses Programms ist folgendes: Wir verfügen über eine abstrakte Basisklasse (MustInherit in Microsoft® Visual Basic®), die die allgemeinen Daten, z. B. das begrenzungsbasierte Rechteck, und eine Reihe von virtuellen Methoden enthält, die meist abstrakt sind (MustOverride in Visual Basic), z. B. Draw. Beachten Sie, dass es wichtig ist, dass Draw polymorph ist, da jeder Typ von ziehbaren Objekten, z. B. Punkt, Linie, Rechteck, Kreis usw., mit völlig anderem Code gezeichnet wird.

Während Methoden polymorph sein können, können Daten nicht. Daher fügen wir nur die Daten in das Programm ein, die wirklich für alle möglichen ziehbaren Objekte gelten – in diesem Fall ein begrenzungsfähiges Rechteck und die Farbe, in der die Linien des Objekts gezeichnet werden sollen.

Daten, die für einen bestimmten Typ von zeichnenden Objekten spezifisch sind, z. B. Mittelpunkt und Radius eines Kreises, Koordinaten der entgegengesetzten Punkte eines Rechtecks oder Endpunkte einer Linie, sollten in der bestimmten Klasse (abgeleitet von der abstrakten Basisklasse) deklariert werden, die diesem Typ des ziehbaren Objekts entspricht. Beachten Sie, dass es möglich ist, eine zweite Ableitungsebene (oder sogar mehrere tiefere Ebenen) zu verwenden, um Ähnlichkeiten zu konsolidieren. Für instance kann der Kreis von Ellipsen abgeleitet werden, da alle Kreise Ellipsen sind. In ähnlicher Weise könnten Quadrate von Rechtecke abgeleitet werden, da alle Quadrate auch Rechtecke (und auch Vierecken und auch Polygone) sind. Die von Ihnen ausgewählte Ableitungsstruktur spiegelt idealerweise nicht nur die Beziehungen zwischen den Klassen wider, sondern auch die gängigen erwarteten Nutzungsmuster, sodass Vorgänge, die Sie häufig ausführen, schnell und einfach sind.

Unten sehen Sie ein Diagramm mit unserer Klassenableitung:

Abbildung 3. Diagramm zur Klassenableitung

Da der Standard Grund für Konstruktoren (Neu in Visual Basic) darin besteht, die Daten zu initialisieren, sind Konstruktoren auch nicht polymorph (kann nicht sein). Das bedeutet, dass der ursprüngliche Erstellungsvorgang nicht polymorph sein kann, was sinnvoll ist, da die Datenanforderungen von Typ zu Typ variieren. Bei gutem Design kann das Objekt jedoch, sobald es erstellt wurde, für den Rest seines Lebens als Polymorph behandelt werden, wie wir es hier tun.

Sehen wir uns an, was sich in dieser Gruppe von Klassen befindet, beginnend mit der abstrakten Stamm-Basisklasse:

Die abstrakte Basisklasse (MustInherit)

Hier sehen Sie den Code für die abstrakte Basisklasse in C#. Dr. GUI nennt es DShape für "drawable shape". Klicken Sie, um die gesamte Quelldatei anzuzeigen.

C#

public abstract class DShape {
   public abstract void Draw(Graphics g);
   protected Rectangle bounding;
   protected Color penColor; // should have property, too
   // should also have methods to move, resize, etc.
}

Und hier ist der entsprechende Code in Visual Basic .NET. Klicken Sie, um die gesamte Quelldatei anzuzeigen.

Visual Basic .NET

Public MustInherit Class DShape
    Public MustOverride Sub Draw(ByVal g As Graphics)
    Protected bounding As Rectangle
    Protected penColor As Color ' should have property, too
    ' should also have methods to move, resize, etc.
End Class

Die Syntax ist anders, aber es ist klar, dass es sich um genau dieselbe Klasse handelt.

Beachten Sie, dass die Draw-Methode implizit virtuell (Überschreibbar) ist, da sie als abstrakt deklariert ist (MustOverride). Beachten Sie auch, dass wir in dieser Klasse keine Implementierung bereitstellen. Da wir nicht wissen, welches Objekt wir in dieser Klasse implementieren, wie können wir zeichnungscode schreiben?

Was sind die Daten?

Beachten Sie auch, dass es nicht viele Daten gibt – aber wir haben alle Daten deklariert, die für eine solche abstrakte Klasse möglich sind.

Jedes gezeichnete Objekt, unabhängig von seiner Form, verfügt über ein begrenzungsfähiges Rechteck , d. h. das kleinste mögliche Rechteck, das das Objekt vollständig einschließen kann. Das umgebende Rechteck wird zum Zeichnen von Punkten (als sehr kleine Rechtecke), Rechtecke und Kreise verwendet und ist für andere Formen als erste sehr schnelle Näherung für Treffer- oder Kollisionstests nützlich.

Es gibt nicht viel anderes, das alle Objekte gemeinsam haben. während das Zentrum für einige Objekte sinnvoll ist, z. B. Kreise und Rechtecke, ist es für andere nicht sinnvoll, z. B. Dreiecke. Und Sie sollten ein Rechteck an seinen Ecken und nicht an seiner Mitte darstellen. Sie können jedoch keinen Kreis an seinen Ecken angeben, da er keine Ecken hat. (Für diese Anwendung stellen wir tatsächlich den Kreis mit seinem umgebenden Rechteck dar, da wir den Mittelpunkt oder den Radius nicht mehr benötigen, sobald der Kreis erstellt wurde, und da die Methode, die wir zum Zeichnen des Kreises verwenden, DrawEllipse, ein umgebendes Rechteck erfordert, um zu wissen, wo der Kreis gezeichnet werden soll.) Dr. GUI vertraut darauf, dass Sie die Schwierigkeit sehen, andere Daten für ein generisches ziehbares Objekt anzugeben. Beachten Sie auch, dass wir, da die Daten privat sind, genau die Daten verwenden können, die für unser Objekt sinnvoll sind, ohne den Benutzern seltsame Daten anzuzeigen.

Jedes ziehbare Objekt hat auch eine Farbe, die den Linien zugeordnet ist, die zum Zeichnen verwendet werden. Daher deklarieren wir dies auch hier. Beachten Sie, dass die Farbe für füllbare Objekte nicht deklariert wurde, da nicht alle Objekte ausfüllbar sind. (Wir ermöglichen es uns weiterhin, die Implementierung und die Schnittstelle gemeinsam zu nutzen, wie Sie später sehen werden.)

Einige abgeleitete Klassen

Wie bereits erwähnt, können wir kein Objekt vom Typ der abstrakten Basisklasse erstellen, obwohl wir jedes von der abstrakten Basisklasse (oder einer beliebigen Basisklasse) abgeleitete Objekt so behandeln können, als sei es ein Basisklassenobjekt.

Um also ein zu zeichnendes Objekt zu erstellen, müssen wir eine neue Klasse von der abstrakten Basisklasse ableiten – und alle abstrakten/MustOverride-Methoden überschreiben.

Das hier gezeigte Beispiel ist die DHollowCircle-Klasse . Die DHollowRectangle-Klasse und die DPoint-Klasse sind sehr ähnlich.

Hier ist DHollowCircle in C#. Klicken Sie, um die anderen Klassen anzuzeigen.

C#

public class DHollowCircle : DShape 
{
   public DHollowCircle(Point p, int radius, Color penColor) {
      p.Offset(-radius, -radius); // need to convert to upper left
      int diameter = radius * 2;
      bounding = new Rectangle(p, new Size(diameter, diameter));
      this.penColor = penColor;
   }

   public override void Draw(Graphics g) {
      using (Pen p = new Pen(penColor)) {
         g.DrawEllipse(p, bounding);
      }
   }
}

Und hier ist die gleiche Klasse in Visual Basic .NET. Klicken Sie, um die anderen Klassen anzuzeigen.

Visual Basic .NET

Public Class DHollowCircle
    Inherits DShape

    Public Sub New(ByVal p As Point, ByVal radius As Integer, _
            ByVal penColor As Color)
        p.Offset(-radius, -radius) ' need to convert to upper left
        Dim diameter As Integer = radius * 2
        bounding = New Rectangle(p, New Size(diameter, diameter))
        Me.penColor = penColor
    End Sub

    Public Overrides Sub Draw(ByVal g As Graphics)
        Dim p = New Pen(penColor)
        Try
            g.DrawEllipse(p, bounding)
        Finally
            p.Dispose()
        End Try
    End Sub
End Class

Beachten Sie, dass wir keine zusätzlichen Daten für diese Klasse deklariert haben. Wie sich herausstellt, sind das umgebende Rechteck und der Stift ausreichend. (Das gilt auch für Punkte und Rechtecke; für Dreiecke und andere Polygone wäre dies jedoch nicht wahr.) Unsere Anwendung muss den Mittelpunkt oder Radius des Kreises nicht kennen, nachdem er festgelegt wurde, daher ignorieren wir sie. (Wenn wir sie wirklich benötigen, könnten wir sie entweder speichern oder aus dem umgebenden Rechteck berechnen.)

Wir benötigen jedoch unbedingt das umgebende Rechteck, da es sich um einen Parameter für die Graphics.DrawEllipse-Methode handelt, die wir zum Zeichnen des Kreises verwenden. Daher berechnen wir das umgebende Rechteck aus dem Mittelpunkt und radius, den wir im Konstruktor übergeben haben. Dies ist ein Beispiel dafür, wie Die Kapselung der Daten ihnen hilft, die besten Entscheidungen bei der Datendarstellung für Ihre Situation zu treffen. Der Benutzer dieser Klasse weiß nichts von umgebenden Rechtecken. Da wir jedoch nur das umgebende Rechteck speichern, ist unser Zeichnungscode effizienter und wir speichern nur eine Darstellung des Kreises.

Sehen wir uns die einzelnen Methoden ausführlich an.

Der Konstruktor

Der Konstruktor akzeptiert drei Parameter: einen Punkt, der die Koordinaten für den Mittelpunkt des Kreises enthält, den Radius des Kreises und eine System.Drawing.Color-Struktur , die die Farbe enthält, in der die Umrisse des Kreises gezeichnet werden sollen.

Anschließend berechnen wir das begrenzungsnde Rechteck aus der Mitte und dem Radius und legen unser Stiftfarbelement auf das übergebene Farbobjekt fest.

Der Zeichencode

Die Draw-Methodenüberladung ist eigentlich ganz einfach: Sie erstellt ein Stiftobjekt aus dem Farbobjekt, das wir im Konstruktor gespeichert haben, und verwendet dann diesen Stift, um den Kreis mit der Graphics.DrawEllipse-Methode zu zeichnen, und übergibt auch das umgebende Rechteck, das wir zuvor erstellt haben.

Grafiken und Stifte und Pinsel, oh MEINE!

Die Grafik-, Stift- und Pinselobjekte verdienen eine kleine Erklärung. (Die Brush-Objekte werden kurz angezeigt, wenn wir beginnen, unsere ausfüllbaren Objekte zu füllen.)

Das Graphics-Objekt stellt einen virtualisierten Zeichnungsraum dar, der einer realen Zeichnungsoberfläche zugeordnet ist. Mit virtualisiert meinen wir, dass Sie dieselben Grafikmethoden verwenden, um auf jedem Oberflächentyp zu zeichnen, den Sie tatsächlich dem Graphics-Objekt zugeordnet haben, auf dem Sie zeichnen. Für diejenigen unter Ihnen, die mit der Programmierung in MFC oder dem Microsoft Windows® SDK vertraut sind, ist das Graphics-Objekt die .NET-Version des sogenannten "Gerätekontexts" (oder "DC") in Windows.

In dieser Windows Form-Anwendung wird das Grafikobjekt , das an Draw übergeben wird, einem Fenster auf dem Bildschirm zugeordnet, insbesondere dem PictureBox-Objekt. Wenn wir genau diesen Code mit unserer Microsoft ASP.NET-Anwendung verwenden, wird das GrafikobjektDraw übergeben einem Bitmapbild zugeordnet. Es kann auch einem Drucker oder anderen Geräten zugeordnet sein. Im Wesentlichen verhält sich Graphics wie eine abstrakte Basisklasse – sie definiert die Schnittstelle, schränkt die Implementierungen jedoch nicht ein. Die verschiedenen Methoden zum Abrufen eines Graphics-Objekts bieten Ihnen verschiedene Typen von Objekten mit unterschiedlichem Verhalten, aber alle implementieren dieselbe Kernschnittstelle.

Der Vorteil dieses Schemas ist, dass wir den exakt gleichen Zeichnungscode verwenden können, um auf verschiedenen Arten von Oberflächen zu zeichnen. Und in unserem Zeichencode müssen wir nichts über die Unterschiede zwischen Bildschirmen, Bitmaps, Druckern usw. wissen – die .NET Framework (und das zugrunde liegende Betriebssystem, Gerätetreiber usw.) kümmern sich um alle Details für uns.

Stifte und Pinsel sind virtualisierte Zeichentools. Ein Stift stellt die Attribute einer Linie dar– Farbe, Breite, Muster und vielleicht sogar eine Bitmap, die zum Zeichnen der Linie verwendet werden kann. Ein Pinsel stellt die Attribute eines gefüllten Bereichs dar– Farbe, Muster und vielleicht sogar eine Bitmap, die zum Ausfüllen eines Bereichs verwendet wird.

Bereinigen nach der Verwendung von (oder zumindest Entsorgen)

Grafik-, Stift- und Brush-Objekte sind Windows-Objekten eines ähnlichen Typs zugeordnet. Diese Windows-Objekte werden im Arbeitsspeicher des Betriebssystems zugeordnet– Arbeitsspeicher, der nicht von der .NET-Runtime verwaltet wird. Wenn diese Objekte für einen langen Zeitraum zugewiesen werden, kann dies zu Leistungsproblemen führen und sogar zu Lackierproblemen unter Microsoft Windows 98 führen, da sich der Grafikheap füllt. Daher sollten Sie die Windows-Objekte so schnell wie möglich freigeben.

Sie fragen sich möglicherweise, warum Sie diese Objekte selbst verwalten müssen, wenn die .NET-Runtime die Speicherverwaltung bereitstellt. Gibt die Runtime die Objekte nicht schließlich frei?

Die .NET-Runtime gibt die Windows-Objekte tatsächlich automatisch frei, wenn das entsprechende .NET Framework-Objekt abgeschlossen und Müll gesammelt wird. Es kann jedoch lange dauern, bis die Garbage Collection erfolgt – und alle möglichen schlechten Dinge, die keine Garbage Collection auslösen, einschließlich des Auffüllens von Windows-Heaps und des Aufbendens aller Datenbankverbindungen, können auftreten, wenn wir diese Objekte nicht sofort freigeben. Dies wäre in der ASP.NET Version dieser Anwendung besonders schlecht, da viele Benutzer auf der gleichen Serverbox auf die Anwendung zugreifen könnten.

Da diese Objekte nicht verwalteten Ressourcen zugeordnet sind, implementieren sie die IDisposable-Schnittstelle . Diese Schnittstelle verfügt über eine Methode, Dispose, die das .NET Framework-Objekt vom Windows-Objekt trennt und das Windows-Objekt freigibt, sodass Ihr Computer glücklich bleibt.

Sie haben nur eine Verpflichtung: Sicherstellen, dass Sie Dispose aufrufen, wenn Sie mit dem Objekt fertig sind.

Die kanonische Form dafür wird im .NET-Code von Visual Basic angezeigt: Sie erstellen das Objekt, verwenden es dann in einem Try-Block und entsorgen das Objekt dann im Block Final . Try /Finally stellt sicher, dass das Objekt auch dann verworfen wird, wenn eine Ausnahme ausgelöst wird. (In diesem Fall kann die von uns aufgerufene Zeichnungsmethode wahrscheinlich keine Ausnahme auslösen, sodass der Try/Finally-Block wahrscheinlich nicht erforderlich ist. Aber es ist gute Praxis, darauf zu kommen, und der gute Arzt wollte Ihnen die richtige Form zeigen.)

Dieses Muster ist so häufig, dass C# eine spezielle Anweisung dafür hat: using. Die using-Anweisung im C#-Code entspricht genau der Deklaration und Try/Finally im Visual Basic .NET-Code , aber viel übersichtlicher, bequemer und weniger fehleranfälliger. (Warum Visual Basic .NET nicht etwas wie die using-Anweisung enthält, geht völlig über Dr. GUI hinaus.)

Die Schnittstelle

Einige, aber nicht alle ziehbaren Objekte können gefüllt werden. Objekte wie Punkte und Linien können nicht gefüllt werden, da sie keine eingeschlossenen Bereiche sind, während Objekte wie Rechtecke und Kreise entweder hohl oder gefüllt sein können.

Andererseits wäre es praktisch, genauso wie wir alle ziehbaren Objekte als Polymorphe behandeln können, alle füllbaren Objekte als Polymorphe behandeln zu können. Für instance wäre es genauso praktisch, wie wir alle ziehbaren Objekte in einer Auflistung platzieren und zeichnen, indem wir die Auflistung durchlaufen und Draw für jedes Objekt aufrufen, auch in der Lage sein, alle ausfüllbaren Objekte in einer Auflistung auszufüllen, unabhängig vom tatsächlichen Typ des ausfüllbaren Objekts. Wir möchten also irgendwie einen Mechanismus wie die Vererbung verwenden, um echte Polymorphie zu erreichen (in einem Leben, trotzdem!).

Da nicht alle ziehbaren Objekte gefüllt werden können, können wir die Fill-Methodendeklaration nicht in die abstrakte Basisklasse einfügen. Die .NET Framework lässt keine mehrfache Vererbung von Klassen zu, sodass wir sie auch nicht in eine andere abstrakte Basisklasse einfügen können. Und wenn wir die füllbaren Objekte von einer anderen Basisklasse als die nicht ausfüllbaren ableiten, dann würden wir die Fähigkeit verlieren, alle ziehbaren Objekte als Polymorphe zu behandeln.

Die .NET Framework unterstützt jedoch Schnittstellen– und eine Klasse kann eine beliebige Anzahl von Schnittstellen implementieren. Die Schnittstellen können keine Implementierung aufweisen – weder Code noch Daten. Die Klasse, die die Schnittstelle implementiert, muss alles bereitstellen.

Alle Schnittstellen können über Deklarationen verfügen. Und so sieht unsere Schnittstelle IFillable in C# aus. Klicken Sie, um die gesamte Quelldatei anzuzeigen.

C#

public interface IFillable {
   void Fill(Graphics g);
   Color FillBrushColor { get; set; }
}

Der entsprechende Visual Basic .NET-Code ist unten dargestellt. Klicken Sie, um die gesamte Quelldatei anzuzeigen.

Visual Basic .NET

Public Interface IFillable
    Sub Fill(ByVal g As Graphics)
    Property FillBrushColor() As Color
End Interface

Wir müssen weder die Methode noch die Eigenschaft virtual/Overridable oder abstract/MustOverride oder irgendetwas deklarieren, da alle Methoden und Eigenschaften in Schnittstellen automatisch öffentlich und abstrakt/MustOverride sind.

Verwenden einer Eigenschaft: Es können keine Daten in einer Schnittstelle vorhanden sein

Beachten Sie, dass wir zwar kein Feld in einer Schnittstelle deklarieren können, aber eine Eigenschaft deklarieren können, da Eigenschaften tatsächlich als Methoden implementiert werden.

Dies belastet jedoch den Implementierer der Schnittstelle, wie wir in Kürze sehen werden. Der Implementierer muss die Methoden get und set sowie alle Daten implementieren, die zum Implementieren der Eigenschaft erforderlich sind. Wenn die Implementierung sehr komplex ist, können Sie möglicherweise eine Hilfsklasse schreiben, um einen Teil davon zu kapseln. Weiter unten im Artikel wird gezeigt, wie Sie eine Hilfsklasse in einem etwas anderen Kontext verwenden können.

Implementieren der Schnittstelle

Nachdem wir nun die Schnittstelle definiert haben, können wir sie in unseren Klassen implementieren. Beachten Sie, dass wir vollständige Implementierungen der von uns implementierten Schnittstellen bereitstellen müssen: Wir können die teile, die wir implementieren möchten, nicht auswählen und auswählen.

Sehen wir uns also den Code für die DFilledRectangle-Klasse in C# an. Klicken Sie hier, um den Code für alle Klassen (einschließlich DfilledCircle) anzuzeigen.

C#

public class DFilledCircle : DHollowCircle, IFillable 
{
   public DFilledCircle(Point center, int radius, Color penColor, 
         Color brushColor) : base(center, radius, penColor) {
      this.brushColor = brushColor;
   }

   public void Fill(Graphics g) {
      using (Brush b = new SolidBrush(brushColor)) {
         g.FillEllipse(b, bounding);
      }
   }
   protected Color brushColor;
   public Color FillBrushColor {
      get {
         return brushColor;
      }   
      set {
         brushColor = value;
      }
   }
   public override void Draw(Graphics g) {
      Fill(g);
      base.Draw(g);
   }
}

Und hier ist der Code für die DFilledRectangle-Klasse in Visual Basic .NET. (Klicken Sie hier, um den Code für alle Klassen (einschließlich DFilledCircle) anzuzeigen.

Visual Basic .NET

Public Class DFilledRectangle
    Inherits DHollowRectangle
    Implements IFillable
    Public Sub New(ByVal rect As Rectangle, _
            ByVal penColor As Color, ByVal brushColor As Color)
        MyBase.New(rect, penColor)
        Me.brushColor = brushColor
    End Sub
    Public Sub Fill(ByVal g As Graphics) Implements IFillable.Fill
        Dim b = New SolidBrush(FillBrushColor)
        Try
            g.FillRectangle(b, bounding)
        Finally
            b.dispose()
        End Try
    End Sub
    Protected brushColor As Color
    Public Property FillBrushColor() As Color _
      Implements IFillable.FillBrushColor
        Get
            Return brushColor
        End Get
        Set(ByVal Value As Color)
            brushColor = Value
        End Set
    End Property
    Public Overrides Sub Draw(ByVal g As Graphics)
        Dim p = New Pen(penColor)
        Try
            Fill(g)
            MyBase.Draw(g)
        Finally
            p.Dispose()
        End Try
    End Sub
End Class

Im Folgenden sind einige Punkte aufgeführt, die Sie zu diesen Klassen beachten müssen.

Abgeleitet von HollowRectangle

Wir haben die gefüllte Klasse von der hohlen Version der -Klasse abgeleitet. Die meisten Änderungen in dieser Klasse: Die Draw-Methode und der Konstruktor sind neu (beide rufen jedoch die Versionen der Basisklasse auf), und wir haben eine Implementierung für die Fill-Methode der IFillable-Schnittstelle und eine Implementierung für die FillBrushColor-Eigenschaft hinzugefügt.

Der Grund, warum wir einen neuen Konstruktor benötigen, ist, dass wir zusätzliche Daten in dieser Klasse haben, die initialisiert werden müssen, nämlich den Füllpinsel. (Sie werden sich an unsere obige Diskussion über Pinsel erinnern.) Beachten Sie, wie dieser Konstruktor den Basisklassenkonstruktor aufruft: In C# ist der "Aufruf" in die Deklaration integriert (: base(center, radius, penColor)); in Visual Basic .NET führen Sie den Aufruf explizit als erste Zeile (MyBase.New(rect, penColor)) der New-Methode (auch als "der Konstruktor" bezeichnet) durch.

Da wir zwei der drei Parameter an den Basisklassenkonstruktor übergeben haben, müssen wir nur das letzte Feld initialisieren.

So ändert sich Das Zeichnen

Sie werden feststellen, dass die Draw-Methode in etwa mit der Basisklasse identisch ist– die Standard Ausnahme besteht darin, dass sie die Fill-Methode aufruft, da ein ausgefülltes Objekt vollständig gezeichnet werden muss. Anstatt den Code zum Zeichnen der Gliederung neu zu schreiben, rufen wir erneut die -Methode der Basisklasse auf: MyBase.Draw(g) in Visual Basic .NET und base.Draw(g); in C#.

Da wir einen Stift zuweisen, mit dem die Gliederung gezeichnet werden soll, müssen wir mit oder Try/Finally und Dispose sicherstellen, dass das Windows-Stiftobjekt sofort freigegeben wird. (Auch hier, wenn wir absolut positiv wären, dass die methoden, die wir aufrufen, niemals eine Ausnahme auslösen würden, könnten wir die Ausnahmebehandlung überspringen und einfach Dispose aufrufen, wenn wir mit dem Stift fertig sind. Aber wir müssenDispose aufrufen, unabhängig davon, ob direkt oder über die using-Anweisung .)

Implementieren der Fill-Methode

Die Fill-Methode ist einfach: Ordnen Sie einen Pinsel zu, füllen Sie dann das Objekt auf dem Bildschirm – und stellen Sie sicher, dass wir den Pinsel entsorgen .

Beachten Sie, dass Sie in Visual Basic .NET explizit angeben müssen, dass Sie eine Methode einer Schnittstelle ("... Implements IFillable.Fill") implementieren, während in C# die Tatsache, dass Sie eine Methode oder Eigenschaft in einer Schnittstelle implementieren, durch die Signatur der Methode oder Eigenschaft bestimmt wird (da Sie eine Methode namens "Fill" geschrieben haben, die nichts zurückgibt und eine Grafik akzeptiert, muss die Implementierung von IFillable.Fill sein). Seltsamerweise bevorzugt Dr. GUI, die im Allgemeinen prägnante Programmierstrukturen bevorzugt (wenn sie keine präzise Schreibweise ist), tatsächlich die Syntax von Visual Basic, da sie sowohl übersichtlicher als auch flexibler ist (der Name der Methode in der implementierenden Klasse in Visual Basic muss nicht mit dem Namen in der Schnittstelle übereinstimmen, und eine bestimmte Methode könnte denkbar mehr als eine Schnittstellenmethode implementieren).

Implementieren der -Eigenschaft

Die IFillable-Schnittstelle enthält auch eine -Eigenschaft, mit der wir die Pinselfarbe festlegen und abrufen können. (Dies wird im Handler Change fills to hot pink button verwendet.)

Um die öffentliche Eigenschaft zu implementieren, benötigen wir ein privates oder geschütztes Feld. Wir haben sie hier geschützt, um den Zugriff von abgeleiteten Klassen zu erleichtern, ohne dass eine Klasse darauf zugreifen kann.

Sobald das Feld vorhanden ist, ist es einfach, ein sehr einfaches Set-Paar zu schreiben und Methoden abzurufen , um die -Eigenschaft zu implementieren. Beachten Sie erneut, dass Sie in Visual Basic .NET explizit angeben müssen, welche Eigenschaft Sie implementieren.

Sie werden auch feststellen, dass die Implementierung dieser Eigenschaft für jede Klasse identisch ist. Später erfahren Sie, wie Sie eine Hilfsklasse verwenden, um diese Implementierung bereitzustellen. In diesem Fall ist der Vorteil minimal (allgemeiner Code für die Implementierung kann Änderungen vereinfachen). In Fällen, in denen die Implementierung der Eigenschaft komplexer ist, können die Vorteile der Verwendung einer Hilfsklasse jedoch groß sein.

Interface oder Abstract (MustInherit)-Basisklasse?

Eines der häufigsten Argumente in der objektorientierten Programmierung ist, ob abstrakte Basisklassen oder Schnittstellen verwendet werden sollen.

Schnittstellen bieten Ihnen zusätzliche Flexibilität, aber zu einem Preis: Sie müssen alles in jeder Klasse implementieren, die diese Schnittstelle implementiert. Wir können eine Hilfsklasse verwenden, um dies zu unterstützen (wir werden später ein entsprechendes Beispiel haben), aber Sie müssen immer noch alles überall implementieren. Und Schnittstellen können keine Daten enthalten (obwohl sie im Gegensatz zum System von Brand J Eigenschaften enthalten können , sodass sie so aussehen können, als ob sie Daten enthalten).

In diesem Fall entschied sich der gute Arzt für die Verwendung einer abstrakten Basisklasse anstelle einer Schnittstelle für DShape , da er die Daten nicht als Eigenschaften in jeder Klasse erneut implementieren wollte. Außerdem wählte er eine Klasse anstelle einer Schnittstelle, da alles, was von DShape abgeleitet wird, "eine" Form ist, wobei die füllbaren Objekte immer noch Formen sind, die auch Füllungen "können".

Ihre Kilometerleistung kann natürlich variieren, aber Dr. GUI denkt, dass er sich für diese Situation ziemlich gut entschieden hat.

Ein Container für unsere Zeichnungsobjekte

Da wir unsere Objekte wiederholt zeichnen (jedes Mal, wenn das Bild in der Windows Forms-Version zeichnet; jedes Mal, wenn die Webseite in der ASP.NET-Version erneut geladen wird), müssen wir sie in einem Container aufbewahren, damit wir immer wieder auf sie zugreifen können.

Dr. GUI ging noch einen Schritt weiter und machte die Container zu intelligenten Containern, die wissen, wie die darin enthaltenen Objekte gezeichnet werden. Hier sehen Sie den C#-Code für diese Containerklasse:

C# (klicken, um die gesamte Datei anzuzeigen)

public class DShapeList {
   ArrayList wholeList = new ArrayList();
   ArrayList filledList = new ArrayList();

   public void Add(DShape d) {
      wholeList.Add(d);
      if (d is IFillable) 
         filledList.Add(d);
   }

   public void DrawList(Graphics g) {
      if (wholeList.Count == 0) 
      {
         Font f = new Font("Arial", 10);
         g.DrawString("Nothing to draw; list is empty...", 
            f, Brushes.Gray, 50, 50);
      }
      else 
      {
         foreach (DShape d in wholeList)
            d.Draw(g);
      }
   }

   public IFillable[] GetFilledList() {
      return (IFillable[])filledList.ToArray(typeof(IFillable));
   }   
}

Und hier ist der Visual Basic .NET-Code für die gleiche Klasse:

Visual Basic .NET (klicken Sie hier, um die gesamte Datei anzuzeigen)

Public Class DShapeList
    Dim wholeList As New ArrayList()
    Dim filledList As New ArrayList()

    Public Sub Add(ByVal d As DShape)
        wholeList.Add(d)
        If TypeOf d Is IFillable Then filledList.Add(d)
    End Sub

    Public Sub DrawList(ByVal g As Graphics)
        If wholeList.Count = 0 Then
            Dim f As New Font("Arial", 10)
            g.DrawString("Nothing to draw; list is empty...", _
               f, Brushes.Gray, 50, 50)
        Else
            Dim d As DShape
            For Each d In wholeList
                d.Draw(g)
            Next
        End If
    End Sub

    Public Function GetFilledList() As IFillable()
        Return filledList.ToArray(GetType(IFillable))
    End Function
End Class

Verwalten von zwei Listen

Da wir die Füllfarbe unserer Objekte ändern möchten, um diese stilvolle Schaltfläche Change fill to hot pink (Füllfläche in heißes Rosa ) zu implementieren, verwalten wir zwei Listen von ziehbaren Objekten: eine Liste mit *ALL*-Objekten und eine Liste nur der ausfüllbaren Objekte. Für beides verwenden wir die ArrayList-Klasse . ArrayList-Objekte enthalten einen Satz von Object-Verweisen , sodass eine ArrayList einen gemischten Beutel eines beliebigen Typs im System enthalten kann.

Das ist eigentlich nicht hilfreich– wir bevorzugen, dass die ArrayList-Objektenur ziehbare/ausfüllbare Objekte enthalten. Um dies zu tun, machen wir die ArrayList-Objekte privat; Dann machen wir den Weg, um den Listen Objekte hinzuzufügen, eine Methode, die wir schreiben, um nur eine DShape zu verwenden.

Wenn wir der Liste mithilfe der Add-Methode Objekte hinzufügen, fügen wir jedes Objekt der wholeList hinzu und überprüfen dann, ob das Objekt auch der filledList-Auflistung hinzugefügt werden soll.

Denken Sie daran, dass die Add-Methode (und damit die Liste) typsicher ist: Sie benötigt nur eine DShape (oder einen von DShape abgeleiteten Typ, z. B. alle oben erstellten Typen). Sie können dieser Liste keine ganze Zahl oder Zeichenfolge hinzufügen. Daher wissen wir immer, dass diese Liste nur ziehbare Objekte enthält. Diese Annahme ist sehr praktisch! Starke Eingabe ist eine gute Sache – eine sehr gute Sache.

Zeichnen der Elemente

Wir verfügen auch über eine Methode namens DrawList , die die Objekte in der Liste für das Graphics-Objekt zeichnet, das als Parameter übergeben wird. Diese Methode hat zwei Fälle: Wenn die Liste leer ist, zeichnet sie eine Zeichenfolge, die besagt, dass die Liste leer ist. Wenn die Liste jedoch nicht leer ist, wird für jedes Konstrukt ein verwendet, um die Liste zu durchlaufen, wobei Draw für jedes Objekt aufgerufen wird. Der Code für die eigentliche Iteration und Zeichnung könnte fast nicht einfacher sein, wie in Visual Basic unten gezeigt.

Visual Basic .NET

Dim d As DShape
For Each d In wholeList
    d.Draw(g)
Next

Der C#-Code ist fast identisch (aber natürlich nimmt er weniger Zeilen an!).

C#

foreach (DShape d in wholeList)
   d.Draw(g);

Da wir wissen, dass die Liste typsicher ist, weil sie gekapselt ist, wissen wir, dass es sicher ist, einfach die Draw-Methode aufzurufen, ohne den Typ des Objekts zu überprüfen.

Zurückgeben der ausfüllbaren Liste

Schließlich benötigt die Schaltfläche "Füllvorgänge ändern" in "Hot Pink " ein Array von Verweisen auf alle ausfüllbaren Objekte, damit die FillBrushColor-Eigenschaft geändert werden kann. Obwohl es möglich wäre, eine Methode zu schreiben, die die Liste iteriert und die Farbe in den übergebenen Wert geändert hat, hat sich der gute Arzt für dieses Mal entschieden, stattdessen ein Array von Objektverweisen zurückzugeben. Glücklicherweise verfügt die ArrayList-Klasse über eine ToArray-Methode , mit der wir ein Array erstellen können, das wir zurückgeben. Diese Methode verwendet den Typ, den die Arrayelemente aufweisen sollen, wodurch wir den gewünschten Typ zurückgeben können– ein Array von IFillable.

C#

public IFillable[] GetFilledList() {
   return (IFillable[])filledList.ToArray(typeof(IFillable));
}   

Visual Basic .NET

Public Function GetFilledList() As IFillable()
    Return filledList.ToArray(GetType(IFillable))
End Function

In beiden Sprachen verwenden wir einen integrierten Operator, um das Type-Objekt für einen bestimmten Typ abzurufen. In C# sagen wir "typeof(IFillable)", in Visual Basic, "GetType(IFillable)".

Das aufrufende Programm verwendet dieses Array, um das Array von Verweisen auf ausfüllbare Objekte zu durchlaufen. Für instance sieht der Visual Basic-Code zum Ändern der Füllfarben in heißes Rosa wie folgt aus:

Dim filledList As IFillable() = drawingList.GetFilledList()
Dim i As IFillable
For Each i In filledList
    i.FillBrushColor = Color.HotPink
Next

Diese Methode dient zu Demonstrationszwecken. Der gute Arzt würde zögern, bevor er diese Methode in Code verwendet, den er geliefert hat. Warum? Nun, dieser Entwurf ist fragwürdig, da er Verweise auf die internen Daten der Liste zurückgibt. (Das zurückgegebene Array ist jedoch eine Kopie von "filledList", sodass zumindest der Aufrufer dies nicht durcheinander bringen kann. Die Verweise beziehen sich jedoch weiterhin auf die tatsächlichen zeichnungsfähigen Objekte.) Es ist in der Regel besser, eine Methode bereitzustellen, um die Änderungen auf eine weise vorzunehmen, die Sie überprüfen können, anstatt Verweise zu verteilen, damit der Aufrufer alles tun kann, was er möchte.

Warum hat Dr. GUI dies nicht anders entworfen? Zunächst besteht die Idee darin, die Polymorphität der Verwendung eines Objekts durch einen Verweis zu demonstrieren, der auf seine Schnittstelle typisiert ist. Das einzige, was der Aufrufer über die -Objekte weiß, ist, dass sie IFillable implementieren. Zweitens ist es schwierig, alle Möglichkeiten vorherzusagen, wie jemand die Objekte bearbeiten möchte. Daher ist es manchmal wünschenswert, eine Methode bereitzustellen, die Verweise auf interne Daten zurückgibt, damit der Aufrufer mehr Flexibilität hat. Wie bei allen technischen Problemen gibt es auch hier Kompromisse – in diesem Fall Flexibilität im Vergleich zu bruchbrechenden Kapselungen.

Hilfsmethoden und -klassen für Factoring Common Code

Möglicherweise haben Sie bemerkt, dass die Draw - und Fill-Methoden viel Code gemeinsam haben Insbesondere der Code zum Erstellen eines Stifts oder Pinsels, zum Einrichten eines Try/Finally-Blocks und zum Entsorgen des Stifts oder Pinsels ist in jeder Klasse gleich – der einzige Unterschied ist die tatsächliche Methode, die zum Zeichnen oder Füllen aufgerufen wird. (Aufgrund der sehr sauber verwendung von Syntax in C# ist die Menge an zusätzlichem Code nicht so offensichtlich.) In Visual Basic .NET gibt es eine eindeutige Codezeile zu fünf Codezeilen, die in allen Implementierungen identisch ist.

Möglicherweise haben Sie auch bemerkt, dass die Implementierung der Eigenschaft in IFillable immer identisch ist.

Wenn Sie über viel wiederholten Code verfügen, möchten Sie im Allgemeinen nach Möglichkeiten suchen, den allgemeinen Code in allgemeine Unterroutinen zu integrieren, die von allen Klassen gemeinsam verwendet werden. Dr. GUI zeigt Ihnen gerne drei (von vielen) möglichen Methoden dazu. Die erste Methode ist nur für Klassen verfügbar. die zweite funktioniert entweder mit Klassen oder Schnittstellen, obwohl wir sie in diesem Beispiel nur mit einer Schnittstelle verwenden.

Hinweis: Wir haben diesen Code nur in Visual Basic und nur für die ASP.NET Version der Anwendung durchgeführt. Aber wir könnten den geänderten Code problemlos in den Windows Forms-Anwendungen verwenden. Dr. GUI hat nur eine Version verwendet, sodass Sie die Unterschiede beim Betrachten des Codes sehen konnten.

Verwenden von Aufrufen einer Hilfsklasse zum Ersetzen von identischem Code

Das erste Problem, mit dem wir uns befassen werden, ist, dass die Eigenschaftenimplementierung für IFillable in allen Klassen identisch ist, die sie implementieren. Wir werden den allgemeinen Code in eine Hilfsklasse integrieren und dann den Hilfsprogramm in unserem Code erstellen und aufrufen. In diesem Fall erfordert das Erstellen und Aufrufen des Helfers so viel Code (eigentlich ein bisschen mehr) als nur die direkte Arbeit. Es ist auch etwas langsamer. Für eine kompliziertere Implementierung der Eigenschaft kann diese Technik jedoch sinnvoll sein. Darüber hinaus ist es einfacher, Code zu verwalten, der in Hilfsprogramme integriert wurde. Wenn Sie eine Änderung am Hilfsprogramm vornehmen müssen, ändern Sie sie an einer Stelle, nicht an jeder Stelle, an der der Code auftritt. In einigen Fällen kann dieser Vorteil die Verwendung einer Hilfsklasse zu einer guten Wahl machen.

Systeme wie C++, die die mehrfache Vererbung von Implementierungen (nicht nur Schnittstellen) unterstützen, können es Ihnen ermöglichen, Hilfsklassen mit geerbten Mix-Ins zu schreiben. Dies ist jedoch in einem System mit nur einer Vererbung wie dem .NET Framework schwieriger– wir müssen die Hilfsklasse direkt instanziieren und explizit darauf zugreifen.

Sehen wir uns zunächst unsere Hilfsklasse an (klicken Sie hier, um den gesamten Code anzuzeigen):

<Serializable()> _
Public Class FillPropHelper
    Private brushColor As Color

    Sub New(ByVal brushColor As Color)
        Me.brushColor = brushColor
    End Sub

    Property FillBrushColor() As Color
        Get
            Return brushColor
        End Get
        Set(ByVal Value As Color)
            brushColor = Value
        End Set
    End Property
End Class

Wir werden später über das <Serializable()> -Attribut sprechen... bitte ignorieren Sie es vorerst.

Es ist einfach, diese Klasse zu betrachten und sie als Implementierung einer Eigenschaft zu sehen. Auch dies ist als faktorierter Code nicht sehr interessant, aber wenn es komplizierter war oder Sie dachten, dass Sie wahrscheinlich das Hilfsprogramm ändern und die Änderungen von allen Klassen, die das Hilfsprogramm verwenden, verwenden möchten, können Sie diese Technik auch für eine so einfache Hilfsklasse verwenden. Dadurch wird Ihr Code jedoch etwas größer und langsamer.

In unserer Clientklasse (DFilledCircle) müssen wir das Hilfsobjekt deklarieren und instanziieren:

    Protected fb As FillPropHelper

    Public Sub New(ByVal center As Point, ByVal radius As Integer, _
            ByVal penColor As Color, ByVal brushColor As Color)
        MyBase.New(center, radius, penColor)
        fb = New FillPropHelper(brushColor) ' uses property helper
    End Sub

Die -Eigenschaft muss weiterhin in unserer Klasse implementiert werden, da sie Teil der IFillable-Schnittstelle ist, die wir implementieren. Diese Implementierung greift nur auf die Eigenschaft in der Hilfsklasse zu.

    Public Property FillBrushColor() As Color Implements _
      IFillable.FillBrushColor
        Get
            Return fb.FillBrushColor
        End Get
        Set(ByVal Value As Color)
            fb.FillBrushColor = Value
        End Set
    End Property

Beachten Sie erneut, dass wir in diesem einfachen Fall keinen Code in der Clientklasse gespeichert haben, die das Hilfsprogramm verwendet. Außerdem haben wir ein zusätzliches Objekt erstellt und Methodenaufrufe durchgeführt, anstatt direkt auf ein Feld in unserer Klasse zuzugreifen. (Vergleichen Sie mit DFilledRectangle.) Da diese spezielle Implementierung keinen Code speichert und langsamer ist, würde Dr. GUI sie wahrscheinlich nicht verwenden, obwohl sie etwas flexibler ist, wenn Sie den Code ändern müssen, der sich mit der Eigenschaft befasst. In komplexeren Fällen kann diese Technik jedoch sehr wertvoll sein.

Verwenden von allgemeinem Code, wenn der Code nicht identisch ist

Ansatz 1: Aufruf der virtuellen Methode für allgemeine Einstiegspunkte

Das nächste Problem, das wir angreifen werden, ist, dass die Draw- und Fill-Methoden fast, aber nicht ganz identisch sind. Dies bedeutet, dass wir eine Möglichkeit haben müssen, ein anderes Verhalten zu tun. Virtuelle Methoden bieten eine Möglichkeit, dies zu tun. Delegaten stellen eine andere bereit. Wir verwenden eine Technik für Draw und eine andere Methode für Fill.

Bei diesem ersten Ansatz nutzen wir auch die Tatsache, dass abstrakte/MustInherit-Klassen im Gegensatz zu Schnittstellen Code enthalten können. Daher stellen wir eine Implementierung der Draw-Methode bereit, die einen Stift zusammen mit dem Ausnahmehandler und Dispose erstellt und dann eine abstrakte/MustOverride-Methode aufruft, die die Zeichnung tatsächlich ausführt. Insbesondere ändert sich die DShapes-Klasse , um diese neue Draw-Methode aufzunehmen und die neue JustDraw-Methode zu deklarieren (klicken Sie, um den gesamten Code anzuzeigen):

Public MustInherit Class DShape
    '   The fact that Draw is NOT virtual is somewhat unusual...usually 
    ' Draw would be abstract (MustOverride).
    '   But this method is the framework for drawing, not the 
    ' drawing code itself, which is done in JustDraw.
    '   Note also that this means that these classes has a different 
    ' interface than the original version, although they 
    ' do the same thing.
    Public Sub Draw(ByVal g As Graphics)
        Dim p = New Pen(penColor)
        Try
            JustDraw(g, p)
        Finally
            p.Dispose()
        End Try
    End Sub
    ' Here's the part that needs to be polymorphic--so it's abstract
    Protected MustOverride Sub JustDraw(ByVal g As Graphics, _
        ByVal p As Pen)
    Protected bounding As Rectangle
    Protected penColor As Color ' should have property, too
    ' should also have methods to move, resize, etc.
End Class

Ein interessanter Hinweis: Die Draw-Methode ist nicht virtuell/überschreibbar. Da alle abgeleiteten Klassen diesen Teil der Zeichnung genau auf die gleiche Weise ausführen (wenn Sie auf einer Grafik zeichnen, die wir per Definition sind, müssen Sie einen Stift unbedingt zuordnen und entsorgen), muss er nicht virtuell/überschreibbar sein.

In der Tat würde Dr. GUI argumentieren, dass Zeichnen in diesem Fall nicht virtuell/überschreibbar sein sollte. Wenn es denkbar wäre, dass Sie das Verhalten von Draw (und nicht nur das Verhalten von JustDraw) außer Kraft setzen möchten, sollten Sie es virtuell/überschreibbar machen. In diesem Fall gibt es jedoch keinen guten Grund, das Verhalten von Draw jemals außer Kraft zu setzen, und es besteht eine Gefahr darin, Programmierer dazu zu ermutigen, es zu überschreiben – sie gehen möglicherweise nicht richtig mit dem Stift um, oder sie können etwas anderes tun, als JustDraw zu rufen, um das Objekt zu zeichnen, wodurch annahmen, die wir in unsere Klasse integriert haben. Wenn Draw nicht virtual ist (eine Option, die wir übrigens nicht einmal in Brand J haben), wird unser Code vielleicht weniger flexibel, aber auch robuster – Dr. GUI ist der Meinung, dass dies in diesem Fall der richtige Kompromiss ist.

Eine typische Implementierung von JustDraw sieht wie folgt aus (diese stammt aus der DHollowCircle-Klasse ; klicken Sie, um den gesamten Code anzuzeigen):

Protected Overrides Sub JustDraw(ByVal g As Graphics, ByVal p As Pen)
    g.DrawEllipse(p, bounding)
End Sub

Wie Sie sehen können, haben wir die Einfachheit der implementierungen abgeleiteter Klassen erreicht, nach der wir gesucht haben. (Die Implementierungen in den ausfüllbaren Klassen sind nur geringfügig komplizierter – das werden wir später sehen.)

Beachten Sie, dass wir unserer Schnittstelle eine zusätzliche öffentliche Methode namens JustDraw hinzugefügt haben, die zusätzlich zum Grafikobjekt, auf dem gezeichnet werden soll, einen Verweis auf das in Draw erstellte Stiftobjekt übernimmt. Da diese Methode abstrakt/MustOverride sein muss, muss sie öffentlich sein.

Dies ist kein großes Problem, aber es ist eine Änderung an der öffentlichen Schnittstelle der -Klasse. Auch wenn dieser Ansatz der Berücksichtigung von allgemeinem Code einfach und bequem ist, können Sie den anderen Ansatz wählen, um zu vermeiden, dass die öffentliche Schnittstelle geändert werden muss.

Ansatz 2: Die virtuelle Methode ruft die allgemeine Hilfsmethode mithilfe des Rückrufs auf.

Wenn es an der Zeit ist, die Fill-Methode der Schnittstelle zu implementieren, ist die Komplikation des Codes ähnlich: Es gibt eine eindeutige Zeile aus sechs Implementierungszeilen. Aber wir können die allgemeine Implementierung nicht in die Schnittstelle einfügen, da Schnittstellen reine Deklarationen sind. sie enthalten weder Code noch Daten. Darüber hinaus ist der oben beschriebene Ansatz nicht akzeptabel, da er die Schnittstelle ändern würde – wir möchten dies möglicherweise nicht tun, oder wir sind sogar nicht in der Lage, dies zu tun, weil eine andere Person die Schnittstelle erstellt hat!

Was wir also tun müssen, ist, eine Hilfsmethode zu schreiben, die das Setup durchführt, und dann in unsere Klasse zurückruft, damit sie tatsächlich die Füllung ausführen kann. In diesem Beispiel hat dr. GUI den Code in eine separate Klasse eingefügt, sodass er von jeder Klasse verwendet werden kann. (Wenn Sie den Ansatz für die Implementierung von Draw verfolgt haben, sollten Sie die Hilfsmethode als private Methode in der abstrakten Basisklasse implementieren.)

Hier ist kurzerhand die Klasse, die wir erstellt haben (klicken Sie, um den gesamten Code anzuzeigen):

' Note how the delegate helps us still have polymorphic behavior.
Class FillHelper
    Public Delegate Sub Filler(ByVal g As Graphics, ByVal b As Brush)
    Shared Sub SafeFill(ByVal i As IFillable, ByVal g As Graphics, _
            ByVal f As Filler)
        Dim b = New SolidBrush(i.FillBrushColor)
        Try
            f(g, b)
        Finally
            b.dispose()
        End Try
    End Sub
End Class

Unsere Hilfsmethode namens SafeFill akzeptiert ein füllbares Objekt (beachten Sie, dass wir hier den IFillable-Schnittstellentyp verwenden, nicht DShape, damit nur ausfüllbare Objekte übergeben werden können!), eine Grafik zum Zeichnen und eine spezielle Variable, die als Delegat bezeichnet wird. Sie können sich einen Delegaten nicht als Verweis auf ein Objekt, sondern auf eine Methode vorstellen. Wenn Sie viel in C oder C++ programmiert haben, können Sie sich ihn als typsicheren Funktionszeiger vorstellen. Der Delegat kann so festgelegt werden, dass er auf eine beliebige Methode verweist, unabhängig davon, ob es sich um eine instance-Methode oder eine statische/shared-Methode handelt, die über die gleichen Typen von Parametern und Rückgabewert verfügt. Sobald der Delegat auf eine geeignete Methode verweist (wie beim Aufruf von SafeFill ), können wir diese Methode indirekt über den Delegaten aufrufen. (Übrigens, da Delegaten in Brand J nicht verfügbar sind, wäre dieser Ansatz erheblich schwieriger und weniger flexibel, wenn Sie ihn verwenden würden.)

Die Deklaration für den Delegattyp Filler befindet sich direkt über der Klassendeklaration. Sie wird als Methode deklariert, die nichts zurückgibt (ein Sub in Visual Basic .NET) und nimmt eine Grafik und einen Brush als Parameter an. In einer zukünftigen Spalte werden die Delegaten ausführlicher erörtert.

Der SafeFill-Vorgang ist einfach: Er weist den Pinsel zu und richtet die Try/Finally und Dispose als gemeinsamen Code ein. Das Variableverhalten wird ausgeführt, indem die Methode aufgerufen wird, auf die vom Delegat verwiesen wird, den wir als Parameter erhalten haben: "f(g, b)".

Um diese Klasse zu verwenden, müssen wir unseren ausfüllbaren Objektklassen eine Methode hinzufügen, die über den Delegaten aufgerufen werden kann, und stellen Sie sicher, dass Sie einen Verweis (die Adresse) dieser Methode an SafeFill übergeben, den wir in der Fill-Implementierung der Schnittstelle aufrufen. Hier sehen Sie den Code für DFilledCircle (klicken Sie hier, um den gesamten Code anzuzeigen):

Public Sub Fill(ByVal g As Graphics) Implements IFillable.Fill
    FillHelper.SafeFill(Me, g, AddressOf JustFill)
End Sub
Private Sub JustFill(ByVal g As Graphics, ByVal b As Brush)
    g.FillEllipse(b, bounding)
End Sub

Wenn das Objekt also gefüllt werden muss, wird IFillable.Fill für das Objekt aufgerufen. Dadurch wird unsere Fill-Methode aufgerufen, die FillHelper.SafeFill aufruft, einen Verweis auf unser Selbst, das Graphics-Objekt , für das wir übergeben wurden, und einen Verweis auf die Methode, die tatsächlich die Füllung durchführt – in diesem Fall unsere private JustFill-Methode .

SafeFill richtet dann den Pinsel ein und ruft über den Delegaten die JustFill-Methode auf, die die Füllung durch Aufrufen von Graphics.FillEllipse ausführt und zurückgibt. SafeFill entsorgt den Pinsel und kehrt zu Fill zurück, was an den Aufrufer zurückgibt.

Schließlich ähnelt JustDrawdem Draw in der ursprünglichen Version sehr, da wir beide Fill aufrufen und die Draw-Methode der Basisklasse aufrufen (dies haben wir zuvor getan). Hier ist der Code dafür (klicken Sie, um den gesamten Code anzuzeigen):

Protected Overrides Sub JustDraw(ByVal g As Graphics, ByVal p As Pen)
    Fill(g)
    MyBase.JustDraw(g, p)
End Sub

Denken Sie daran, dass die Komplikation der Zuweisung der Pinsel und Stifte in den Hilfsfunktionen behandelt wird – im Fall von Draw in der Basisklasse; im Fall von Fill in der Hilfsklasse.

Wenn Sie der Meinung sind, dass dies etwas komplexer ist als zuvor, haben Sie recht. Wenn Sie denken, dass es aufgrund der zusätzlichen Anrufe und der Notwendigkeit, sich mit einem Delegaten zu beschäftigen, etwas langsamer als zuvor ist, sind Sie wieder richtig. Im Leben geht es um Kompromisse.

Lohnt es sich also? Vielleicht. Es hängt davon ab, wie kompliziert der allgemeine Code ist und wie oft er dupliziert wird. mit anderen Worten, es ist ein Kompromiss. Wenn wir beschlossen hätten , das Try/Finally-Objekt zu entfernen und den Stift und den Pinsel einfach zu entsorgen, nachdem wir mit dem Zeichnen fertig waren, wäre dieser Code so einfach gewesen, dass keiner dieser Ansätze gerechtfertigt gewesen wäre (es sei denn, der Wartungsvorteil war wichtig). Und in C# ist die using-Anweisung einfach genug, dass wir uns wahrscheinlich auch nicht mit diesen Ansätzen zu stören möchten. Dr. GUI ist der Ansicht, dass der Visual Basic-Fall mit Try/Finally auf der Grenze liegt, ob es sich lohnt, einen dieser Ansätze zu verwenden oder nicht, aber er wollte Ihnen diese Ansätze zeigen, falls Sie Situationen mit komplizierterem Code haben.

Serialisierbare Objekte

Um sie mit ASP.NET verwenden zu können, müssen wir eine weitere Änderung an unseren zeichenbaren Objektklassen vornehmen: Sie müssen serialisierbar sein, damit die Daten zwischen der Standard-Webseite und der Webseite, die das Bild generiert, übergeben werden können (weitere Informationen dazu später). Serialisierung ist der Prozess des Schreibens der Daten für eine Klasse auf ein Speichermedium so, dass sie gespeichert und/oder übergeben und später deserialisiert werden können. Die Deserialisierung ist der Prozess, bei dem das Objekt aus den serialisierten Daten neu erstellt wird. Wir werden dies in einer späteren Spalte ausführlicher besprechen.

Als Dr. GUI diese Anwendung ursprünglich als Windows Forms-Anwendung schrieb, verwendete er nur die in den Pinsel- und Stiftklassen verfügbaren Stockpinsel und Stifte, die vom .NET Framework und dem Betriebssystem vorab zugewiesen werden. Da diese bereits zugeordnet sind, schadet das Beibehalten von Verweisen auf sie nichts, und sie müssen nicht "Disposed" sein.

Aber Stifte und Pinsel, da sie ziemlich komplizierte Objekte sein können, sind nicht serialisierbar, sodass der gute Arzt seine Strategie ändern musste. Er entschied sich, stattdessen die Farbe der Stifte und Pinsel zu speichern und Stifte und Pinsel im Fliegen zu erstellen, da die Objekte gezeichnet und gefüllt werden müssen.

Wie machen Sie die Dinge serialisierbar?

Serialisierung ist ein großer Teil der .NET Framework, sodass das Framework die Serialisierung von Objekten vereinfacht.

Alles, was wir tun müssen, um eine Klasse serialisierbar zu machen, ist, sie mit dem Serializable-Attribut zu markieren. (Dies ist die gleiche Art von Attribut, die wir zuvor für unsere Enumeration verwendet haben, um es als Eine Reihe von Flags zu markieren.) Die Syntax in C# und Visual Basic .NET lautet wie folgt:

C#

[Serializable]
class Foo // ...

Visual Basic .NET

<Serializable()> _
Class Foo ' ...

Hinweis: Zusätzlich zum Markieren der Klasse serialisierbar müssen alle Daten, die Ihre Klasse enthält, serialisierbar sein, sonst löst das Serialisierungsframework eine Ausnahme aus, wenn Sie versuchen, die Daten zu serialisieren.

Damit der Container auch serialisierbar ist

Eines der coolen Dinge am .NET Framework ist, dass die Containerklassen serialisierbar sind. Das bedeutet, dass der Container automatisch auch serialisierbare Objekte in diesen speichern kann.

In unserem Fall enthält die DShapeList-Klasse also zwei ArrayList-Objekte . Da ArrayList serialisierbar ist, müssen wir DShapeList serialisierbar machen, um es wie folgt mit dem Serializable-Attribut zu markieren:

Visual Basic .NET

<Serializable()> _
Public Class DShapeList
    Dim wholeList As New ArrayList()
    Dim filledList As New ArrayList()
    ' ...

C#

[Serializable]
public class DShapeList {
   ArrayList wholeList = new ArrayList();
   ArrayList filledList = new ArrayList();

Sofern die Objekte, die wir in der DShapeList platzieren, alle serialisierbar sind, können wir die gesamte Liste mit einer einzigen Anweisung serialisieren und deerialisieren!

Übrigens wäre dies eine gute Änderung für die Windows Forms Versionen der Anwendungen, da es uns ermöglichen würde, unsere Zeichnungen in eine Datenträgerdatei zu schreiben und sie wieder einzuladen.

Drei Versionen von gezeichneten Objekten; jede kann in jedem Kontext verwendet werden.

Sie haben wahrscheinlich bereits bemerkt, dass wir über drei Versionen unseres Code für ziehbare Objekte verfügen: jeweils eine in C# und Visual Basic .NET, die nicht die hilfsweisen Methoden verwenden, die wir oben geschrieben haben, und eine in Visual Basic .NET, die die Hilfsprogramme verwendet.

Es gibt einen weiteren geringfügigen Unterschied: Die Datenklassen in der Datei mit den Hilfsprogrammen sind als serialisierbar gekennzeichnet; Die Datenklassen in den anderen Dateien sind nicht.

Beachten Sie jedoch diesen sehr wichtigen Punkt: Wenn wir zurückgingen und die Datenklassen in allen Dateien Serializable markiert haben, könnten wir jede der Klassen mit einer der Anwendungen verwenden. Wir könnten C# und Visual Basic .NET kombinieren. Außerdem könnten wir Code verwenden, der ursprünglich für eine Windows Form-Anwendung mit einer ASP.NET-Anwendung geschrieben wurde.

Diese Art der einfachen Wiederverwendung von Code bedeutet, dass der code, den Sie schreiben, wertvoller ist, da er an so vielen verschiedenen Stellen wiederverwendet werden kann.

Verwenden unserer zeichnenden Objekte in einer Windows Forms-Anwendung

Nachdem wir nun die ziehbaren Objektklassen erläutert haben, gehen wir darauf ein, wie wir sie in einer Windows Forms-Anwendung verwenden. Lassen Sie uns zunächst ein wenig darüber sprechen, wie Windows Forms Anwendungen funktionieren.

Hauptteile einer Windows Forms-Anwendung

Einfache Windows Forms Anwendungen bestehen aus einem Standard Fenster oder Formular, das untergeordnete Elemente enthält, die Steuerelemente sind. Wenn Sie ein Visual Basic-Programmierer sind, werden Sie dieses Modell sehr vertraut finden.

Das Hauptfenster

Das Schlüsselobjekt in einer Windows Forms Anwendung ist das Standard Fenster. Dieses Formular wird in der statischen/Shared Main-Methode Ihrer Anwendung erstellt, wie unten gezeigt.

In einer einfachen Windows Forms Anwendung wie wir schreiben, sind alle anderen Steuerelemente untergeordnete Elemente dieses Standard Formulars.

Schaltflächen und Textfelder

Unser Formular verfügt über eine Reihe von Schaltflächen und einige Textfelder. Jede Schaltfläche verfügt über einen Handler, der bewirkt, dass der Liste und der Liste ein Shape hinzugefügt wird. Die Textfelder sind enthalten, um Ihnen zu zeigen, wie Sie Eingaben aus einem Formular erhalten. Außerdem gibt es ein Gruppenfeld, um einen visuellen Hinweis auf die Textfelder und die zugeordneten Schaltflächen bereitzustellen.

PictureBox

Auf der linken Seite befindet sich das wichtigste Steuerelement von allen: die PictureBox. Hier wird das Bild gezeichnet und angezeigt. In einer Windows-Anwendung müssen Sie in der Lage sein, das Bild jederzeit neu zu zeichnen. Für instance müssen Sie, wenn das Fenster minimiert oder von einem anderen Fenster abgedeckt wird, neu zeichnen, wenn das Fenster erneut verfügbar gemacht wird.

Diese bedarfsgesteuerte Zeichnung erfolgt als Reaktion auf die Paint-Nachricht, die von einem Ereignishandler in der klasse des übergeordneten Formularfensters behandelt wird.

Hauptroutinen in einer Windows Forms-Anwendung

Werfen wir einen kurzen Blick auf die wichtigen Routinen in unserer Windows Forms-Anwendung. Beachten Sie, dass der Code für die Benutzeroberfläche im Vergleich zum Code für die ziehbaren Objekte recht kurz ist. Das ist die Macht, dass die .NET Framework eine Menge der Arbeit für Sie erledigen. (Und es zeigt, dass wir mit den Klassen der ziehbaren Objekte ziemlich gute Arbeit geleistet haben.)

Formularmethoden

Das Formular oder Standard Fensters wird von System.Windows.Forms.Form abgeleitet, sodass es sein gesamtes Verhalten erbt. Alle Steuerelemente werden als Member dieser Klasse deklariert, sodass sie verworfen werden, wenn die Klasse verworfen wird (was tatsächlich explizit in der Dispose-Methode erfolgt).

Es enthält auch Deklarationen für die benötigten Daten ( DShapeList und ein Zufallszahlengeneratorobjekt), Main und Ereignishandler für Schaltflächenklicks und das Paint-Ereignis des PictureBox-Objekts.

Main

Der Auftrag von Main besteht darin, einfach das Standard-Fensterobjekt zu erstellen und auszuführen. Der Code hierzu in C# ist unten aufgeführt.

C# (klicken Sie, um den gesamten Code anzuzeigen)

[STAThread]
static void Main() 
{
   Application.Run(new MainWindow());
}

Das STAThread-Attribut ist für Main of Windows Forms-Anwendungen wichtig. Sie sollten es immer verwenden, damit funktionen, die von OLE-Automatisierung (z. B. Drag-and-Drop und die Zwischenablage) verwendet werden, ordnungsgemäß funktionieren.

Sie finden diese Methode nicht im Visual Basic .NET-Quellcode, der von Microsoft Visual Studio® für Sie generiert wurde. Wenn Sie jedoch in der generierten .exe mit ILDASM suchen, finden Sie einen Main , der dasselbe wie der oben genannte tut – wahrscheinlich vom Visual Basic .NET-Compiler generiert.

InitializeComponent

Unter Windows Form Designer generierten Code (klicken Sie auf das kleine Pluszeichen, wenn der Code in dieser Region nicht angezeigt wird), wird der Code zum Erstellen und Initialisieren aller Schaltflächen und anderen Steuerelemente im Formular angezeigt. Sie können hier klicken, um den gesamten Code für C# und für Visual Basic .NET einschließlich dieser Region anzuzeigen.

Datendeklarationen/Zufallszahlengenerierung

Zusätzlich zu allen Steuerelementen, die im ausgeblendeten Bereich Ihres Codes deklariert sind, müssen wir auch zwei Variablen deklarieren: die Datenstruktur, die unsere Zeichnungsliste enthält, und ein Objekt vom Typ Random. Wir verwenden das Random-Objekt , um Zufallszahlen für die Positionen von Objekten zu generieren, die wir erstellen. Sie können die Referenzdokumentation zu zufälligen Klassen überprüfen.

Die Datendeklarationen befinden sich innerhalb der MainWindow-Klasse , aber außerhalb einer beliebigen Methode. In C# und Visual Basic .NET sieht es wie folgt aus:

C#

DShapeList drawingList = new DShapeList();
Random randomGen = new Random();

Visual Basic .NET

Dim drawingList As New DShapeList()
Dim randomGen As New Random()

Wir haben auch eine Hilfsmethode geschrieben, um einen zufälligen Punkt zu erhalten:

C#

private Point GetRandomPoint() {
   return new Point(randomGen.Next(30, 320), randomGen.Next(30, 320));
}

Visual Basic .NET

Private Function GetRandomPoint() As Point
    Return New Point(randomGen.Next(30, 320), randomGen.Next(30, 320))
End Function

Es generiert zwei Zufallszahlen zwischen 30 und 320, die als Koordinaten für unseren Zufallspunkt verwendet werden.

Ereignishandler für Schaltflächenklick

Als Nächstes haben wir einen Schaltflächen-Klickhandler für jede Schaltfläche. Die meisten von ihnen fügen der Zeichnungsliste einfach ein neues ziehbares Objekt hinzu, und rufen Invalidate im PictureBox auf, sodass es mithilfe der aktualisierten Zeichnungsliste neu übermalt wird. Der Code für einen typischen Schaltflächenhandler lautet wie folgt:

C#

private void AddPoint_Click(object sender, System.EventArgs e)   {
    drawingList.Add(new DPoint(GetRandomPoint(), Color.Blue));
    Drawing.Invalidate();
}

Visual Basic .NET

Private Sub AddPoint_Click(ByVal sender As System.Object, _
        ByVal e As System.EventArgs) Handles AddPoint.Click
    drawingList.Add(New DPoint(GetRandomPoint(), Color.Blue))
    Drawing.Invalidate()
End Sub

Die Schaltfläche "Füllungen ändern" in "Hot Pink" ist etwas anders: Sie ruft ein Array aller ausfüllbaren Objekte in der Liste ab, und ändert dann ihre Pinselfarbe in hot pink. Der Code dafür wird oben am Ende des Abschnitts Zurückgeben der ausfüllbaren Liste angezeigt. (Sie müssen auch die PictureBox ungültig machen.)

Schließlich erstellt die Schaltfläche Alle löschen einfach eine neue Zeichnungsliste und zeigt das Feld drawingList darauf. Dadurch wird die alte Zeichnungsliste für die spätere Garbage Collection freigegeben. Anschließend wird das PictureBox-Element ungültig, sodass es ebenfalls gelöscht wird.

PictureBox Paint-Ereignishandler

Das letzte Element, um das wir uns kümmern müssen, ist das Malen des Bilds in der PictureBox. Dazu behandeln wir das Paint-Ereignis, das von PictureBox generiert wird, und verwenden das Grafikobjekt , auf dem wir von diesem Ereignis übergeben werden, um zu zeichnen. Um die Zeichnung zu machen, rufen wir einfach die DrawList-Methode der Zeichnungsliste auf . Für jede Schleife und Polymorphie kümmern Sie sich um den Rest!

C# (Klicken Sie hier, um den gesamten Code anzuzeigen).

private void Drawing_Paint(object sender,
      System.Windows.Forms.PaintEventArgs e) {
   drawingList.DrawList(e.Graphics);
}

Visual Basic .NET (Klicken Sie hier, um den gesamten Code anzuzeigen).

Private Sub Drawing_Paint(ByVal sender As Object, _
        ByVal e As System.Windows.Forms.PaintEventArgs) _
        Handles Drawing.Paint
    drawingList.DrawList(e.Graphics)
End Sub

Dies schließt unsere Tour durch die Windows Forms Anwendungen ab – spielen Sie mit dem Code und ändern Sie ihn einige, um mehr zu erfahren!

Verwenden unserer zeichnenden Objekte in einer ASP.NET-Anwendung

Obwohl es einige Unterschiede zwischen ASP.NET Webanwendungen und Windows Forms Anwendungen gibt, ist das Erstaunliche an Dr. GUI, wie ähnlich die beiden Arten von Anwendungen sind! Wenn Sie also mit Visual Basic-Formularen oder Windows-Anwendungen vertraut sind, sollten Sie in diesem Abschnitt den Übergang zu ASP.NET Webanwendungen erleichtern.

Übrigens können Sie auch wieder die ASP.NET-Anwendung ausführen.

Hauptteile einer Web Forms-Anwendung

Die Standard Teile der ASP.NET Web Forms Anwendung entsprechen eng den Teilen der Windows Forms Anwendung.

Die Seite

Dies entspricht dem Standard Fenster in der Windows Forms-Anwendung. Die Seite ist der Container für alle Schaltflächen und andere Steuerelemente.

Schaltflächen

Auch hier gibt es eine Reihe von Schaltflächen, die dazu führen, dass Aktionen im Formular ausgeführt werden. Beachten Sie, dass wir im Gegensatz zu unseren vorherigen Anwendungen die pageLayout-Eigenschaft des Dokuments der Seite auf GridLayout und nicht auf FlowLayout festlegen. Das bedeutet, dass wir jede Schaltfläche (und ein anderes Steuerelement) exakt nach Pixelposition positionieren können.

Beachten Sie, dass es auch einige Textfelder gibt.

Beachten Sie auch, dass Sie Windows Forms-Steuerelemente nicht in ein Webformular kopieren und einfügen können. Sie müssen die Seite erneut erstellen.

Bildsteuerung

Das Bildsteuerelement entspricht dem PictureBox-Steuerelement in der Windows Forms-Anwendung. Es gibt jedoch einige wichtige Unterschiede: Anstatt Paint-Nachrichten zu generieren, enthält das Bildsteuerelement die URL, aus der wir unser Bild laden.

Wir legen diese URL auf eine zweite Webseite, ImageGen.aspx, fest. Mit anderen Worten, wir haben eine Webseite, deren gesamte Aufgabe darin besteht, die Bits im Bild aus unserer Zeichnungsliste zu generieren und dieses Bild an den Webbrowser des Clients zu senden.

Im Folgenden wird der Code für diesen Code erläutert.

Hauptroutinen einer Web Forms-Anwendung

Es gibt einige wichtige Unterschiede zwischen dem Code für Windows Forms Anwendungen und Web Forms Anwendungen – aber es gibt auch einige interessante Ähnlichkeiten. Beachten Sie außerdem, dass der gesamte Code in der Datei für zeichnende Objekte für jede der drei Anwendungen verwendet werden kann.

Unsere Seite ist von System.Web.UI.Page abgeleitet und enthält eine Reihe von Deklarationen für alle Steuerelemente, zusätzlich zu:

Alles gleich: Datendeklarationen und GetRandomPoint

Dieser Code entspricht fast dem in der Visual Basic .NET-Windows Forms-Anwendung. Wenn Sie möchten, sehen Sie sich es oben noch einmal an. Der einzige Unterschied besteht darin, dass die Felder deklariert, aber nicht initialisiert werden. Sie werden in der Page_Load-Methode initialisiert (siehe unten).

Die GetRandomPoint-Methode ist genau die gleiche wie in den anderen Anwendungen. Code wiederverwenden zu können, ist doch cool, oder?

Sehr ähnlich: Schaltflächenklickhandler

Die Schaltflächen-Klickhandler sind mit einer Ausnahme die gleichen wie in der Windows Forms-Anwendungen: Da das Bild jedes Mal neu gezeichnet wird, wenn eine Schaltfläche auf das Webformular geklickt wird, besteht keine Notwendigkeit (und keine Möglichkeit), das Bildsteuerelement zu "ungültig" zu machen. Stattdessen wird es automatisch neu gezeichnet, sodass der einzige Aufruf der Add-Methode der Zeichnungsliste ist.

Hier ist ein typischer Schaltflächenhandler – dieser für einen Punkt (klicken Sie, um den gesamten Code anzuzeigen):

Private Sub AddPoint_Click(ByVal sender As System.Object, _
        ByVal e As System.EventArgs) Handles AddPoint.Click
    drawingList.Add(New DPoint(GetRandomPoint(), Color.Blue))
End Sub

Die anderen Schaltflächenhandler ähneln dem Windows Forms Fall, mit der Ausnahme natürlich, dass keine Methode aufgerufen wird, um das Bild zu ungültig zu machen.

Sehr unterschiedlich: Seitenlade- und Entladehandler

Völlig neu sind die Methoden zum Laden und Entladen von Seitenhandlern.

Denken Sie daran, dass unser Seitenobjekt mit jeder HTTP-Anforderung neu erstellt wird. Da sie mit jeder Anforderung neu erstellt wird, können wir keine Daten als Membervariablen speichern, wie im Windows Forms Fall, in dem das Standard Fenster vorhanden ist, solange die Anwendung vorhanden ist.

Daher müssen wir die benötigten Informationen zwischen Anforderungen und seitenübergreifend in einer Art Zustandsvariable speichern. Hier gibt es mehrere Optionen – wir werden dies als Nächstes besprechen.

Übergeben des Zustands zwischen Seiten und Anforderungen

Damit unsere Anwendung funktioniert, muss sie in der Lage sein, den Zustand zwischen Anforderungen beizubehalten und diesen Zustand an das Zeichenblatt zu übergeben (siehe unten).

Es gibt mehrere Möglichkeiten, den Zustand beizubehalten und zu übergeben. Wenn es sich bei unserer Anwendung ausschließlich um eine Einzelseitenanwendung handelt (wie bei früheren Anwendungen), können Sie den Ansichtszustand verwenden, bei dem die Daten in einem ausgeblendeten Eingabefeld auf der Webseite codiert sind.

Aber unser Bildsteuerelement erstellt seine Zeichnung auf einer separaten Seite, sodass wir etwas flexibler benötigen. Unsere beste Wahl sind Cookies und sitzungszustand. Der Sitzungsstatus ist sehr flexibel, erfordert jedoch serverbasierte Ressourcen. Der Browser verwaltet Cookies, aber sie sind in ihrer Größe sehr begrenzt.

Page_load

Page_Load wird aufgerufen, nachdem das Seitenobjekt erstellt wurde, aber bevor Ereignishandler ausgeführt werden. Daher ist unsere Page_Load-Methode ein perfekter Ort, um zu versuchen, unsere persistenten Daten zu laden. Wenn wir sie nicht finden, erstellen wir neue Daten. Der Code lautet wie folgt:

    Private Sub Page_Load(ByVal sender As System.Object, _
            ByVal e As System.EventArgs) _
            Handles MyBase.Load
        randomGen = ViewState("randomGen")
        If randomGen Is Nothing Then randomGen = New Random
        ' Option 1: use Session state to get drawing list 
        ' (saved in Page_Unload)
        ' (Note: requires state storage on server)
        drawingList = Session("drawingList")
        If drawingList Is Nothing Then drawingList = New DShapeList

        ' Option 2: Session state using SOAP (or base 64) serialization
        ' Note: helpers convert to string; might be better to stop at
        ' stream or byte array....
        'Dim tempString As String = Session("drawinglist")
        'If tempString Is Nothing Then
        '   drawingList = New DShapeList
        'Else
        '    ' or DeserializeFromBase64String...
        '    drawingList =__
        '        SerialHelper.DeserializeFromSoapString(tempString)
        'End If

        ' Option 3: retrieve drawing state from cookie 
        ' on the user's browser
        ' (Note: no server storage, but some users disable cookies)
        ' (2nd note: deserialization is not automatic with cookies! :( )
        ' (3rd note: Using a cookie will severely limit the number 
        '   of(shapes you can draw)
        'Dim drawingListCookie As HttpCookie
        'drawingListCookie = Request.Cookies("drawingList")
        'If drawingListCookie Is Nothing Then
        '   drawingList = New DShapeList
        'Else
        '    drawingList = _
        '        SerialHelper.DeserializeFromBase64String( _
        '           drawingListCookie.Value)
        '    ' another option: DeserializeFromSoapString, but don't--
        '    ' see article
        'End If
    End Sub

Zunächst versuchen wir, den Zustand des Zufallsgenerators aus dem Ansichtszustand zu laden. Wenn er vorhanden ist, verwenden wir den gespeicherten Wert. Wenn dies nicht der Fall ist, erstellen wir ein neues Random-Objekt .

Als Nächstes versuchen wir, die Zeichnungsliste aus dem Sitzungszustand zu laden. Wenn keine Zeichnungsliste vorhanden ist, erstellen wir eine leere Liste.

Sowohl der Ansichtszustand als auch der Sitzungszustand serialisieren unsere Objekte bei Bedarf automatisch. Der Ansichtszustand wird immer serialisiert, sodass er als codierte Zeichenfolge in einem ausgeblendeten Eingabefeld im Browser dargestellt werden kann. Der Sitzungsstatus wird serialisiert, wenn er in einer Datenbank gespeichert oder zwischen Servern übergeben werden soll. Er wird jedoch nicht serialisiert, wenn Ihre Anwendung auf einem einzelnen Server ausgeführt wird, wie beim Testen auf Ihrem Entwicklungscomputer.

Der auskommentierte Code "Option 2" würde die Zeichnungsliste aus dem Sitzungszustand laden, jedoch entweder mithilfe der SOAP-Serialisierung oder der Base64-Codierung der binären Serialisierung. In beiden Fällen konvertiert dieser Code den serialisierten Stream in eine Zeichenfolge. Dies ist nicht erforderlich, um Ihre Daten im Sitzungszustand zu speichern, da der Sitzungszustand automatisch serialisiert wird. Es kann jedoch interessant sein, die SOAP-Darstellung Ihrer Daten zu lesen – sie könnten dies im Debugger einmal versuchen.

Der auskommentierte Code "Option 3" versucht, die Zeichnungsliste aus einem Cookie zu laden. Beachten Sie, dass der Umgang mit einem Cookie wesentlich komplizierter ist als der Umgang mit dem Ansichts- oder Sitzungszustand. Zum einen erhalten Sie keine automatische Serialisierung. Um in eine Zeichenfolge zu serialisieren, haben wir die Hilfsfunktion in eine neue Klasse geschrieben, wie unten gezeigt (klicken Sie, um den gesamten Code anzuzeigen):

Public Shared Function DeserializeFromBase64String( _
        ByVal base64String As String) As Object
    Dim formatter As New BinaryFormatter()
    Dim bytes() As Byte = Convert.FromBase64String(base64String)
    Dim serialMemoryStream As New MemoryStream(bytes)
    Return formatter.Deserialize(serialMemoryStream)
End Function

Dr. GUI verwendete einen Binärformatierer und konvertierte in eine druckbare Base64-Zeichenfolge, da weder die SOAP- noch XML-Formatierungsprogramme für diese Serialisierung in Cookies gut funktionierten. Wir mussten aus einer reinen binären Darstellung in eine Basiszeichenfolge mit 64 konvertieren, um potenzielle Probleme mit Steuerzeichen in der Zeichenfolge zu vermeiden, wenn wir die Bytes einfach kopiert haben. Die Basiszeichenfolge 64 verwendet ein Zeichen A-Z, a-z, 0-9, + oder / (das sind 64 oder 2^6 Zeichen), um alle sechs Bits in der Binärenzeichenfolge darzustellen. Daher stellen vier Zeichen drei Bytes dar– das erste Zeichen stellt sechs Bits im ersten Byte, das zweite Zeichen die letzten beiden Bits des ersten Byte und die ersten vier Bits des zweiten Byte dar. Und so weiter. Auch hier geht es darum, dass wir potenzielle Probleme mit Steuerzeichen vermeiden, indem wir unsere Zeichenfolge auf druckbare Zeichen beschränken.

Der XML-Formatierer serialisiert keine privaten Daten, und Dr. GUI war nicht dabei, öffentlichen Zugriff auf die privaten Daten in der Zeichnungsliste hinzuzufügen. Der SOAP-Formatierer hat diese Einschränkung nicht, sondern erzeugt so große Zeichenfolgen, dass selbst ein oder zwei Zeichnungsobjekte die Kapazität eines Cookies überlaufen würden. (Version 1.0 der .NET Framework hatte einen bösen Fehler, bei dem der SOAP-Formatierer eine leere Liste nicht serialisiert, sodass sie deserialisiert werden konnte. Stattdessen wurde nichts in den Stream für die leere Liste geschrieben, was dazu führte, dass eine Ausnahme auftritt, wenn wir versuchten, deserialisieren, aber dieser Fehler wurde in Version 1.1 behoben.)

Aus Gründen der Lesbarkeit hätte der gute Arzt es vorgezogen, in einem lesbaren XML-Format zu serialisieren, aber da keiner der XML-Serialisierungsformatierer die Aufgabe für Cookies erledigen würde, entschied er sich für den Binärformatierer und eine Konvertierung in eine Basis-64-Zeichenfolge.

Beachten Sie jedoch, dass die Größenbeschränkung für Cookies es unmöglich macht, viele Daten zu speichern, die sie verwenden. Die Verwendung des Sitzungszustands funktioniert normalerweise viel besser, es sei denn, die Daten sind sehr klein.

Page_Unload

Page_Unload wird aufgerufen, bevor das Seitenobjekt, einschließlich aller enthaltenen Daten, zerstört wird. Daher ist es ein guter Ort, um unsere wichtigen Daten zu speichern, damit wir sie später in Page_Load (oder im Page_Load des Bilds) abrufen können.

Daher speichern wir die Daten in Page_Unload und rufen sie in Page_Load ab. Scheint ein wenig seltsam, aber es ist die richtige Sache zu tun.

Hier ist unser Code für Page_Unload (klicken Sie, um den gesamten Code anzuzeigen):

    Private Sub Page_Unload(ByVal sender As Object, _
            ByVal e As System.EventArgs) _
            Handles MyBase.PreRender
        ViewState("randomGen") = randomGen

        ' Option 1: write session state using automatic serialization
        Session("drawingList") = drawingList

        ' Option 2: session state using SOAP (or base 64) serialization
        ' Since session state can serialize anything, didn't have to 
         ' convert to string--but needed string for Option 3
         ' Session("drawingList") = _' (or use SerializeToBase64String)
         '   SerialHelper.SerializeToSoapString(drawingList)

        ' Option 3: write a cookie. Must write code to serialize.
        ' NOTE: Using a cookie will severely limit the number of shapes 
        ' you can draw--not even usuable with SOAP serialization because
        ' cookie size is limited and SOAP serialization takes a lot of
        ' space.
        'Dim drawingListString As String = _
        '    SerialHelper.SerializeToBase64String(drawingList)
        '' another option: SerializeToSoapString, but don't--see article
        'Response.Cookies.Add(New HttpCookie("drawingList", _
        '    drawingListString))
    End Sub

Dieser Code ist etwas einfacher, da wir nicht überprüfen müssen, ob sich der Zustand bereits in der Ansicht oder im Sitzungszustandsobjekt befindet. wir schreiben es einfach bedingungslos aus.

Auch hier: Während sich der Ansichtszustand und der Sitzungszustand automatisch serialisieren, tun Cookies dies nicht, sodass wir dies selbst tun müssen. Dr. GUI schrieb die folgende Hilfsfunktion (in einer separaten Klasse), wobei der code hier gezeigt wird:

Public Shared Function SerializeToBase64String(ByVal o As Object) _
        As String
    Dim formatter As New BinaryFormatter()
    Dim serialMemoryStream As New MemoryStream()
    formatter.Serialize(serialMemoryStream, o)
    Dim bytes() As Byte = serialMemoryStream.ToArray()
    Return Convert.ToBase64String(bytes)
End Function

(Im vollständigen Code sind auch Hilfsfunktionen für die SOAP-Serialisierung vorhanden.)

Zeichnen in einer separaten Seite

Wie bereits erwähnt, erfolgt die Zeichnung auf einer separaten Seite. Hier ist der Code für diese Seite (klicken Sie, um den gesamten Code anzuzeigen):

    Private Sub Page_Load(ByVal sender As System.Object, _
            ByVal e As System.EventArgs) Handles MyBase.Load
        Dim drawingList As DShapeList

        ' Get the drawing list:
        ' Option 1: use session state w/ automatic serialization
        drawingList = Session("drawingList")
        If drawingList Is Nothing Then drawingList = New DShapeList

        ' Option 2: session state with SOAP/base 64 serialization
        ' (See main page code for more comments)
        'Dim tempString As String = Session("drawinglist")
        'If tempString Is Nothing Then
        '   drawingList = New DShapeList
        'Else
        '    ' or DeserializeFromBase64String...
        '    drawingList = _
        '       SerialHelper.DeserializeFromSoapString(tempString)
        'End If

        ' Option 3: use a cookie...
        ' (See main page code for more comments)
        'Dim drawingListCookie As HttpCookie
        'drawingListCookie = Request.Cookies("drawingList")
        'If drawingListCookie Is Nothing Then
        '   drawingList = New DShapeList
        'Else
        '    drawingList = _
        '        SerialHelper.DeserializeFromBase64String( _
        '        drawingListCookie.Value)
        '    ' another option: DeserializeFromSoapString, but don't--
        '    ' see article
        'End If

Zunächst erhalten wir die Zeichnungsliste aus dem Sitzungszustand oder einem Cookie. (Der Code dafür ähnelt der obigen Page_Load-Methode . Eine Erklärung finden Sie im obigen Abschnitt.)

Als Nächstes legen wir den ContentType des Antwortstreams, den wir schreiben, als GIF-Bild fest.

Danach machen wir etwas wirklich Witziges: Wir erstellen eine Bitmap von der gewünschten Größe (in diesem Fall die gleiche Größe wie in der Windows Forms Anwendung).

Anschließend erhalten wir ein Dieser Bitmap zugeordnetes Graphics-Objekt , löschen es und zeichnen unsere Liste darauf.

Hier ist der wichtige Teil: Als Nächstes schreiben wir den Bildinhalt im GIF-Format in den Antwortstream (d. h. in den Browser). Wir legen den Antworttyp fest, um sicherzustellen, dass der Browser das Bild ordnungsgemäß interpretiert, und sendet dann die Bits des Bilds. (Die .NET Framework macht dies erstaunlich einfach. Das Zeichnen auf einer Bitmap war ein königlicher Schmerz in den alten Windows GDI-Tagen!

Der andere wichtige Teil besteht darin, die Graphics- und Bitmap-Objekte zu entfernen und ein Try/Finally-Objekt zu verwenden, damit die Objekte auch dann gelöscht werden, wenn eine Ausnahme ausgelöst wird.

Puh! Das war viel, aber es hat sich gelohnt, diese Anwendung als ASP.NET-Anwendung auszuführen – und noch besser, eine Anwendung, die kein clientseitiges Skript verwendet. Probieren Sie es aus! Sie können die Anwendung ausführen.

Versuch es doch mal!

Wenn Sie .NET haben, können Sie es ausprobieren. Falls nicht, erwägen Sie es bitte. Wenn Sie eine Stunde oder so eine Woche mit Dr. GUI .NET verbringen, werden Sie ein Experte im .NET Framework sein, bevor Sie es wissen.

Seien Sie der Erste in Ihrem Block – und laden Sie einige Freunde ein!

Es ist immer gut, der Erste zu sein, der eine neue Technologie lernt, aber noch mehr Spaß macht es mit einigen Freunden! Organisieren Sie für mehr Spaß eine Gruppe von Freunden, um .NET zusammen zu lernen!

Einige Dinge, die Sie ausprobieren können...

Probieren Sie zunächst den hier gezeigten Code aus. Einige davon stammen aus größeren Programmen; Es empfiehlt sich, das Programm um diese Codeausschnitte herum zu erstellen. (Oder verwenden Sie den Code, den der gute Arzt bereitstellt, wenn Sie dies müssen.) Spielen Sie mit dem Code einige.

Fügen Sie dem Zeichenprogramm einige verschiedene Formen hinzu, gefüllt und nicht ausgefüllt. Hinweis: Obwohl wir es noch nicht behandelt haben, können sich die hinzugefügten Klassen in einer anderen Assembly (und damit in einer anderen ausführbaren Datei) als die anderen Dateien befinden. Das bedeutet, dass die von Ihnen hinzugefügten Klassen sogar in einer anderen Sprache als der Rest enthalten können. Sie erhalten zusätzliche Anerkennung für die Lese- und Arbeit, die erforderlich ist, um diese Arbeit zu machen.

Verwenden Sie in der Windows Forms Version dieses Programms die Serialisierung, um Ihre Daten in eine Datei zu schreiben, damit Sie Zeichnungen speichern und laden können. Für eine echte Herausforderung finden Sie heraus, wie Sie drucken.

Fügen Sie den Klassen hier einige Methoden hinzu, und fiddle mit der Benutzeroberfläche. Machen Sie sich ein süßes kleines CAD-Programm.

Verwenden Sie Vererbung, abstrakte/MustInherit-Klassen , Schnittstellen und Polymorphismus in einem Projekt Ihrer Wahl. Die Vererbung funktioniert am besten, wenn die Familie von Klassen viel gemeinsam hat. Schnittstellen funktionieren am besten, wenn die Klassen nicht viel gemeinsam haben, aber die Funktionalität ähnlich ist.

Versuchen Sie, Ihre eigenen Zeichen-Apps in ASP.NET zu erstellen. Wenn Sie die .NET Framework ausführen können, benötigen Sie nur Microsoft Internet Information Server (IIS), um ASP.NET auf Ihrem eigenen Computer auszuführen – kein Server erforderlich! Dr. GUI hält es für unbeschreiblich cool, Webanwendungen auf seinem Drei-Pfund-Laptop mit nur dem Standardbetriebssystem und dem kostenlosen .NET Framework erstellen und testen zu können. (Aber der gute Arzt bevorzugt auch Visual Studio oder mindestens Visual Basic oder Microsoft Visual C# ® Standard Edition.) Look Ma! Kein Server! Sie benötigen nicht einmal eine Internetverbindung! (Versuchen Sie es mit der erweiterten Edition von Brand J....)

Yo! Lassen Sie uns reden!

Haben Sie Fragen oder Kommentare zu dieser Spalte? Besuchen Sie das Dr. GUI .NET Message Board auf GotDotNet.

Was wir getan haben; Was kommt als nächstes

Dieses Mal haben wir ein umfassenderes Beispiel für Vererbung, abstrakte (MustInherit)-Basisklassen und Schnittstellen in einer einfachen Zeichnungsanwendung gesehen – und wir haben dies sowohl mit Windows Forms als auch mit ASP.NET Web Forms getan.

Nachdem wir nun wissen, wie Vererbung, Schnittstellen und Polymorphismus funktionieren, werden wir uns beim nächsten Mal mit der Mutter aller .NET Framework Klassen befassen: System.Object. Wenn wir Zeit haben, werden wir auch die Speicherverwaltung, einschließlich Garbage Collection und Dispose, besprechen.