Windows-Forms-Anwendungen

Veröffentlicht: 25. Apr 2006

Von Milena Salman

Mit Windows Forms können Sie komfortable und reaktionsschnelle Benutzeroberflächen für Ihre Anwendungen erstellen. Dieser Artikel stellt einige Verfahren vor, mit denen Sie zudem ein optimales Leistungsverhalten von Windows-Forms-basierten Anwendungen gewährleisten können. Es geht hierbei um übliche leistungskritische Szenarios wie zum Beispiel Starten der Anwendung sowie Füllen und Zeichnen von Steuerelementen. Außerdem erfahren Sie, wie Sie eine Anwendung hinsichtlich Leistung entwerfen und kodieren. Insgesamt sollten Sie mit diesen Verfahren eine solide Grundlage schaff en können, um den höchsten Nutzeffekt mit Ihrer Benutzeroberfläche zu erzielen.

 

msdn Magazin


Diesen Artikel können Sie dank freundlicher Unterstützung von msdn Magazin auf MSDN Online lesen. msdn Magazin ist ein Partner von MSDN Online.
Ausgabe: April/Mai 2006

 


Zur Partnerübersichtsseite von MSDN Online

Beispielcode Download: WindowsFormsPerformance.exe (605 KB)

Auf dieser Seite

Spritziger Start
Schnelleres Laden der Benutzeroberfläche
Steuerelemente füllen
Datenbindung optimieren
Layout abspecken
Leistung beim Zeichnen
Text und Bilder
Zeichentechniken anwenden
Ressourcen verwalten

Spritziger Start

Die Zeit für den Startvorgang ist für die meisten Anwendungen eine wichtige Leistungskenngröße. Ein schneller Start hinterlässt beim Benutzer einen guten Eindruck in Bezug auf Leistung und Brauchbarkeit der Anwendung. Zwei Begriff e beschreiben, wie eine Anwendung startet: Warmstart und Kaltstart. Wenn Sie eine verwaltete Anwendung unmittelbar nach dem Neustart Ihres Computers aufrufen, dann die Anwendung schließen und erneut starten, bemerken Sie gewöhnlich einen deutlichen Unterschied zwischen den beiden Startzeiten. Der erste (Kalt-)Start muss Festplatten-E/A-Operationen ausführen, um alle erforderlichen Seiten in den Speicher zu laden. Der zweite (Warm-)Start ist beträchtlich schneller, weil er die bereits im Hauptspeicher vorhandenen Seiten wieder verwendet.

Um den Warmstarteffekt zu erhalten, müssen Sie nicht unbedingt dieselbe Anwendung vorher ausgeführt haben. Allein das Starten irgendeiner verwalteten Anwendung bringt viele Seiten aus den Microsoft-.NET-Framework-Binärdateien in den Speicher, was die Startzeit für alle verwalteten Anwendungen verbessert, die Sie danachstarten.

Wenn Sie Ihren Entwurf auf Leistung ausrichten ist es wichtig, zwischen diesen beiden Szenarios zu unterscheiden und getrennte Ziele für sie festzulegen. Während sich der Warmstart durch eine geringere CPU-Nutzung verbessern lässt, wird der Kaltstart vorwiegend durch Festplatten-E/A beeinflusst. Demzufolge sind bestimmte Optimierungen in bestimmten Szenarios wirkungsvoll, in anderen dagegen nicht.

Zuerst gibt Ihnen dieser Artikel einige allgemeine Leistungsempfehlungen für eine startende Anwendung, die in beliebig verwaltetem Code geschrieben ist. Dann erhalten Sie einige Tipps speziell für die Leistungsverbesserung von Windows-Forms-basierten Anwendungen. Detaillierte Richtlinien zur Verbesserung der Startleistung von verwalteten Anwendungen finden Sie in der CLR-Inside-Out-Kolumne von Claudio Caldato in der Februar-Ausgabe 2006 des MSDN Magazine unter msdn.microsoft.com/msdnmag/issues/06/02/CLRinsideOut.

Das Kaltstartverhalten lässt sich verbessern, wenn man die Anzahl der DLLs verringert, die eine Anwendung lädt. Das beeinflusst direkt die Anzahl der Seiten, die beim Starten der Anwendung bewegt werden müssen. Zusätzlich verringert sich durch die niedrigere Anzahl der geladenen Module der CPU-Overhead, der mit dem Laden eines Moduls verbunden ist, was sich ebenfalls günstig auf das Warmstartverhalten auswirkt.

Wie Module in Ihre Anwendung geladen werden, können Sie mit dem Ereignisfilter Load Module im WinDbg-Debugger des Plattform-SDK diagnostizieren. Alternativ können Sie mit dem folgenden WinDbg-Befehl ermitteln, warum ein bestimmtes Modul geladen wurde:

sxe ld:foo.dll

Sehen Sie sich die Aufrufstapel an und stellen Sie fest, ob sich einige der Modulladevorgänge vermeiden lassen. Hier bietet sich auch die Option ca ml des verwalteten Debuggers MDbg an, der als Teil des .NET Framework SDK verfügbar ist. In manchen Fällen lässt sich das Laden zusätzlicher Module vermeiden, indem Sie einfach Ihren Code umgestalten. Lädt Ihre Anwendung massenhaft DLLs, deren Besitzer Sie sind, sollten Sie daran denken, diese DLLs – dort wo es sinnvoll erscheint – zu weniger größeren DLLs zusammenzufassen.

Die Just-In-Time (JIT)-Kompilierung von Methoden kann viele CPU-Zyklen verbrauchen und wird beim Anwendungsstart leicht zu einem Flaschenhals. Um diesen Overhead zu vermeiden, können Sie Assemblys mithilfe von Native Image Generator (NGen.exe, siehe auch msdn.microsoft.com/msdnmag/issues/05/04/NGen) vorkompilieren. NGen beherrscht alle Arbeiten des JIT-Compilers, erledigt diese aber vorab, legt die Änderungen auf Festplatte ab und spart somit CPU-Zyklen zur Laufzeit. Wenn Sie Ihre Binärdateien mit NGen vorkompilieren, bringt das einen zusätzlichen Leistungsvorteil: Native Codeseiten lassen sich zwischen Prozessen gemeinsam nutzen, während JIT-compilierte Codeseiten für einen Prozess privat sind und sich nicht wieder verwenden lassen.

Trotz ihrer Vorteile ist die Vorkompilierung kein Allheilmittel für einen spritzigen Start. Der Warmstart profitiert wahrscheinlich von der Vorkompilierung mit NGen. Dagegen muss das Kaltstartverhalten nicht unbedingt besser sein und fällt möglicherweise sogar etwas langsamer aus, weil die Seiten mit dem kompilierten Code jetzt von der Festplatte geladen werden müssen. Wenn Sie aber die JIT-Kompilierung vollständig unterdrücken, dürfte auch der Kaltstart schneller laufen, da die für den JIT-Compiler selbst erforderlichen Seiten nicht geladen werden müssen. Es empfiehlt sich, die Kalt- und Warmstartzeiten zu messen, um den Einfluss von NGen abschätzen zu können.

Eine andere Technik installiert stark benannte Assemblys im globalen Assemblycache (GAC), um die Signaturüberprüfung von starken Namen zu vermeiden. Das ist ein langwieriger Prozess, der jede Seite der Assembly inspiziert, bevor sie geladen wird. Wenn Sie sich für NGen entscheiden, ist es sogar noch wichtiger, die Variante mit stark benannten Assemblys im GAC zu verwenden und damit zu vermeiden, dass Basisadressen für DLLs neu zugewiesen werden müssen.

Jede Binärdatei (DLL oder EXE) besitzt eine bevorzugte Basisadresse – eine Position im virtuellen Speicher, wohin sie zu laden ist. Diese Adresse legt der Compiler zur Erstellungszeit fest. Wenn Sie den Visual-Basic- oder C#-Compiler verwenden, erhalten alle Ihre Binärdateien die gleiche Basisadresse (0x400000), wenn keine expliziten Erstellungsdirektiven etwas anderes festlegen. Somit kommt Ihre EXE zur Ladezeit an diese Adresse und alle Ihre DLLs müssen an anderen Stellen im virtuellen Speicher platziert (mit einer neuen Basisadresse versehen) werden, da ihre bevorzugte Basisadresse belegt ist.

Wenn eine DLL eine neue Basisadresse erhält, aktualisiert das Ladeprogramm alle absoluten Adressen in der DLL, um der neuen Ladeadresse zu entsprechen. Das bedeutet, dass jede Seite mit einer anzupassenden Adresse zu inspizieren ist, wenn die betreffende DLL eine neue Basisadresse erhalten hat. Und damit sich die Seite überschreiben lässt, muss sie in eine Auslagerungsdatei kopiert und zurückgelesen werden. Zu diesem Zeitpunkt ist die Seite privat für den Prozess und lässt sich nicht mit anderen Prozessen gemeinsam nutzen.

Um die Leistungsspitze infolge der Neuzuweisung von Basisadressen zu vermeiden, können Sie mit der Compileroption /baseaddress explizit eine bevorzugte Basisadresse für jede DLL in Ihrer Anwendung festlegen. Weisen Sie Basisadressen an DLLs beginnend von einem vorbestimmten Ausgangspunkt (zum Beispiel 0x10000000) zu und lassen Sie ausreichende Lücken zwischen den Adressen, um den DLLs etwas Platz für ein eventuelles Wachstum zu schaffen.

NGen-Abbilder sind für gewöhnlich beträchtlich größer als Intermediate Language (IL)-Abbilder, was den Einfluss auf das Neuzuweisen von Basisadressen vergrößert. NGen setzt die gleiche Basisadresse für das erzeugte Abbild wie die im IL-Abbild spezifizierte Adresse. Wenn Sie also Ihre Assemblys mit NGen erstellen und Basisadressen für Ihre IL-Abbilder zuweisen, sollten Sie ausreichend Platz, der den NGen-Abbildern entspricht, lassen. Normalerweise brauchen Sie drei- oder viermal mehr Platz für ein NGen-Abbild als für das äquivalente IL-Abbild.

Ob Ihre Basisadressenzuweisung ordnungsgemäß funktioniert hat, können Sie überprüfen, indem Sie die tatsächlichen Ladeadressen verifizieren und mit den bevorzugten Ladeadressen vergleichen. Dazu können Sie die Beispielanwendung TList.exe und den Microsoft COFF Binary File Dumper (Dumpbin.exe) einsetzen. Der folgende Befehl zeigt die tatsächliche Ladeadresse für jede im Kontext der Anwendung mit der Prozess-ID 1240 geladene DLL an:

tlist 1240

Mit Dumpbin überprüfen Sie die bevorzugte Standardadresse für Ihre DLL wie folgt:

dumpbin /headers test.dll

In der Ausgabe dieses Befehls finden Sie OPTIONAL HEADERS mit einer Image Base (Abbildbasisadresse) aufgelistet, wie zum Beispiel:

75F70000 image base (75F70000 to 75F78FFF).

Das bedeutet, dass die bevorzugte Basisadresse von test.dll gleich 75F70000 ist und der Adressbereich von 75F70000 bis 75F78FFF verfügbar sein sollte, um diese DLL bei ihrer bevorzugten Basisadresse zu laden.

Schnelleres Laden der Benutzeroberfläche

Die wahrgenommene Startleistung hängt in großem Maße von der Verzögerung ab, bis das erste Element der Benutzeroberfläche erscheint. Deshalb sollten Sie die erforderliche Logik für die Anzeige der Benutzeroberfläche auf ein Minimum reduzieren. Dazu gehören sämtliche Arbeiten, die im Load-Ereignis des Formulars spezifiziert sind, weil diese ablaufen, wenn sich das Formular selbst anzeigt.

Falls Sie teure Netzwerk- oder Datenbank-Aufrufe verwenden, sollten Sie diese asynchron auf einem anderen Thread ausführen, um das Blockieren der Benutzeroberfläche zu vermeiden. In Windows Forms 2.0 können Sie mit der BackgroundWorker-Komponente Arbeit auf einen Hintergrundthread verlagern. In Listing 1 werden mithilfe von BackgroundWorker rekursiv alle Dateien gesucht, die einem gegebenen Muster (zum Beispiel *.txt) in einem gegebenen Ordner und seinen Unterordnern entsprechen. Der Hintergrundthread aktualisiert den UI-Thread mit dem Fortschritt der Operation und der UI-Thread fügt als Reaktion auf die laufenden Aktualisierungen die Dateien oder Unterordner in eine TreeView-Darstellung ein. Die resultierende TreeView wird gefiltert, sodass Unterordner, die keine übereinstimmenden Dateien enthalten, nicht zu sehen sind.

Listing 1: Einen Hintergrundthread verwenden

Imports System.IO

Public Class Form1
      Private DirInfo As DirectoryInfo
      Private Pattern As String

      Private Sub Form1_Load(ByVal sender As System.Object, _
                    ByVal e As System.EventArgs) Handles MyBase.Load
             DirInfo = My.Computer.FileSystem.GetDirectoryInfo(“C:\\“)
             Pattern = “*.txt“
             Me.Text = “Searching for “ & Pattern & “ files“
             Me.TreeView1.Sorted = True
             Me.BackgroundWorker1.WorkerReportsProgress = True
             Me.BackgroundWorker1.RunWorkerAsync()
      End Sub

      Private Sub BackgroundWorker1_DoWork( _
             ByVal sender As System.Object, _
             ByVal e As System.ComponentModel.DoWorkEventArgs) _
             Handles BackgroundWorker1.DoWork

             For Each f As FileInfo In DirInfo.GetFiles(Pattern)
                    Dim treeNode As TreeNode = New TreeNode(f.ToString)
                    Me.BackgroundWorker1.ReportProgress(0, treeNode)
             Next

             For Each SubDir As DirectoryInfo In DirInfo.GetDirectories()
                    Dim treeNode As TreeNode = GetMatchingFiles(SubDir)
                    If Not treeNode Is Nothing Then
                           Me.BackgroundWorker1.ReportProgress(0, treeNode)
                    End If
             Next
      End Sub

      Private Sub BackgroundWorker1_ProgressChanged(_
             ByVal sender As Object, _
             ByVal e As System.ComponentModel.ProgressChangedEventArgs) _
             Handles BackgroundWorker1.ProgressChanged

             Me.TreeView1.Nodes.Add((CType(e.UserState, TreeNode)))
      End Sub

      Private Sub BackgroundWorker1_RunWorkerCompleted( _
             ByVal sender As Object, _
             ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) _
             Handles BackgroundWorker1.RunWorkerCompleted

             Me.Text = “Ready“
      End Sub

      Private Function GetMatchingFiles(ByVal dir As DirectoryInfo) _
             As TreeNode

             Dim treeNode As TreeNode = Nothing
             For Each f As FileInfo In dir.GetFiles(Pattern)
                    If treeNode Is Nothing Then
                           treeNode = New TreeNode(dir.Name)
                    End If
                    treeNode.Nodes.Add(New TreeNode(f.ToString))
             Next
             For Each SubDir As DirectoryInfo In dir.GetDirectories()
                    Dim subNode As TreeNode = GetMatchingFiles(SubDir)
                    If Not subNode Is Nothing Then
                           If treeNode Is Nothing Then
                                  treeNode = New TreeNode(dir.Name)
                           End If
                           treeNode.Nodes.Add(subNode)
                    End If
             Next
             Return treeNode

      End Function
End Class

Ein Hintergrundthread bringt hier den Vorteil, dass die Benutzeroberfläche vollständig zugänglich ist, während die Suche aktiv läuft. Der Benutzer kann durch die Liste der bislang gefundenen Dateien blättern und Unterordnerknoten erweitern, die der Tree-View hinzugefügt wurden.

Operationen, die zwar ausgeführt werden müssen, aber für die Anzeige der ersten Benutzeroberfläche nicht erforderlich sind, lassen sich im Leerlauf des Systems abwickeln oder auf Anforderung ausführen, nachdem das erste UI-Element zu sehen ist. Beispielsweise füllen Sie bei einem TabControl nur die oberste Seite während des Startvorgangs und rufen die Informationen für andere Seiten nach Bedarf ab. Der Code in Listing 2 umfasst ein Formular mit einem TabControl, das 6 TabPages (Tabseiten) enthält. Alle für die einzelnen TabPages erforderlichen Steuerelemente sind in einem UserControl untergebracht, das einer Steuerelementauflistung der TabPages hinzugefügt wird, wenn die TabPage in den Vordergrund kommt. Um den Code einfach zu halten, wird das gleiche UserControl für alle TabPages verwendet und lediglich der Kontext bestimmter untergeordneter Steuerelemente etwas geändert. Allerdings ist es problemlos möglich, für jede TabPage ein eigenes UserControl vorzusehen.

Für die oberste Seite wird das UserControl im Konstruktor des Formulars hinzugefügt. Für die anderen Seiten geschieht dies als Reaktion auf das Ereignis TabControl.SelectedIndexChanged, wenn die korrespondierende Seite angezeigt werden soll. Der Benutzer bemerkt eine geringe Verzögerung, wenn die TabPage erstmalig erscheint. Wählt er die TabPage später erneut aus, gibt es diese Verzögerung nicht mehr, weil die TabPage bereits initialisiert ist.

Dieses Beispiel bremst absichtlich das Erstellen der TabPage, indem eine ListView mit sehr vielen Einträgen angezeigt wird. Wenn man alle TabPages vorab initialisiert, beträgt die erforderliche Zeit bis zur Anzeige eines Formulars auf dem verwendeten System (Pentium III, 800 MHz, 512 MB RAM) rund 9 Sekunden. Füllt man die TabPages auf Anforderung, braucht jede TabPage bei der erstmaligen Anzeige etwa 1,5 Sekunden. Wird nur die oberste Seite gefüllt, erscheint das gesamte Formular also in 1,5 Sekunden.

Dies lässt sich noch ein wenig weiter treiben, indem der Code auf das Ereignis Application.Idle reagiert und jeweils eine Tab-Page füllt, wenn das Idle-Ereignis aufritt. Wird das Formular oder eine neue TabPage angezeigt, braucht der Benutzer eine gewisse Zeit, um das Formular optisch zu erfassen und den Mauszeiger zu verschieben, um eine andere Seite auszuwählen. Während dieser subjektiv bedingten Verzögerung im Ablauf der Anwendung kommt das Idle-Ereignis zum Zuge und der Ereignishandler kann zusätzliche TabPages füllen.

Lässt sich die Startleistung mit anderen Verfahren nicht zufrieden stellend verbessern, sollten Sie Startbildschirme („Splash Screens“) und Fortschrittsleisten einsetzen, um den Benutzer auf dem Laufenden zu halten. Ein entsprechendes Beispiel finden Sie unter msdn.microsoft.com/library/en-us/dnnetcomp/html/casoast.asp. Wenn Sie Ihre Anwendung in Visual Basic schreiben, können Sie die Eigenschaft My.Application.SplashScreen des .NET Framework 2.0 verwenden (siehe hierzu msdn2.microsoft.com/1t310742.aspx).

Steuerelemente füllen

Viele Windows-Forms-Steuerelemente wie ListView, TreeView und ComboBox zeigen Auflistungen von Elementen an. Fügt man diesen Auflistungen zu viele Elemente hinzu, ist ein Flaschenhals die Folge, sofern man keine Optimierungen anwendet. Zum Glück wurden mit .NET Framework 2.0 mehrere Verbesserungen zum Füllen von Steuerelementen in Windows Forms eingebaut. Dazu gehört, dass AddRange-Methoden intern mit den Methoden BeginUpdate und EndUpdate arbeiten, was speziell das Füllen von TreeView mit AddRange und allgemein das sortierte Füllen aller Steuerelemente optimiert.

Der häufigste Grund für langsames Füllen von Steuerelementen ist das Neuzeichnen des Steuerelements nach jeder Änderung. Mehrere Windows Forms-Steuerelemente implementieren BeginUpdate- und EndUpdate-Methoden, die das Neuzeichnen unterdrücken, während die zugrunde liegenden Daten oder Steuerelementeigenschaften manipuliert werden. Durch diese Methoden ist es möglich, umfangreiche Änderungen an einem Steuerelement vorzunehmen (beispielsweise Einträge in seine Auflistung einzufügen) und dabei zu vermeiden, dass das Steuerelement jedes Mal neu gezeichnet wird. Der folgende Code zeigt, wie sich diese Methoden verwenden lassen:

listView1.BeginUpdate();
for(int i = 0; i < 10000; i++)
{
    ListViewItem listItem = new ListViewItem(“Item“+i.ToString() );
    listView1.Items.Add(listItem);
}
listView1.EndUpdate();

Massenoperationen sind immer effizienter als Einzeländerungen. Den Auflistungen von Steuerelementen wie ComboBox, ListView oder TreeView fügt man Elemente vorzugsweise mit der AddRange-Methode hinzu, die es erlaubt, ein Array von vorab erstellten Elementen in einem Zug einzufügen. Windows Forms unternimmt alle möglichen Optimierungen, damit diese Operation effizient abläuft. Oftmals bedeutet dies einfach, dass BeginUpdate und EndUpdate automatisch aufgerufen werden. In manchen Fällen finden allerdings zusätzliche Optimierungen statt. Zum Beispiel sind diese Optimierungen beim TreeView-Steuerelement deutlich zu spüren.

Ein ListView-Steuerelement wird erheblich schneller gefüllt, nachdem das Handle von ListView erstellt worden ist. Müssen Sie ein Formular anzeigen, das ein ListView mit vielen Elementen enthält, fügen Sie die Elemente in das ListView im Form .Load- oder Form.Show-Ereignishandler hinzu. Zu diesem Zeitpunkt ist das ListView-Handle bereits vorhanden. Wenn Sie ListView-Einträge im Konstruktor des Formulars hinzufügen (d.h. bevor das ListView-Handle existiert), werden die Einträge zwar schnell hinzugefügt, doch dauert das Anzeigen des Formulars infolge der langsamen ListView-Wiedergabe beträchtlich länger.

Im Unterschied zum ListView-Steuerelement wird das TreeView deutlich schneller gefüllt, wenn man die Knoten hinzufügt, bevor das Handle erstellt ist. Wenn Sie TreeView.AddRange verwenden, bemerken Sie den Unterschied nicht – das TreeView wird schnell gefüllt, unabhängig davon, wann Sie das tun. Wenn Sie jedoch die Add-Methode verwenden und Knoten im Konstruktor des Formulars hinzufügen, bevor das Handle von TreeView existiert, wird Ihr TreeView gefüllt und genauso schnell angezeigt, als hätten Sie die AddRange-Methode verwendet.

Datenbindung optimieren

Beim Füllen datengebundener Steuerelemente wie zum Beispiel ComboBox oder ListBox ist es effizienter, die DataSource-Eigenschaft erst dann zu setzen, wenn ValueMember und DisplayMember festgelegt sind. Andernfalls wird Ihr Steuerelement als Ergebnis der ValueMember-Änderung erneut gefüllt:

comboBox1.ValueMember = “Name“;
comboBox1.DisplayMember = “Name“;
comboBox1.DataSource = test;

Der eben gezeigte Code ist deshalb effizienter als der folgende:

comboBox1.DataSource = test;
comboBox1.ValueMember = “Name“;
comboBox1.DisplayMember = “Name“

BindingSource.SuspendBinding und BindingSource.ResumeBinding sind zwei Methoden, mit denen sich die Datenbindung zeitweise unterbrechen und dann fortsetzen lässt. Die Methode SuspendBinding bewirkt, dass Änderungen erst zur Datenquelle gelangen, wenn ResumeBinding aufgerufen wird.

Diese Methoden sind für Szenarios mit einfacher Bindung wie zum Beispiel TextBox- oder ComboBox-Datenbindung gedacht. Der Code in Listing 3 demonstriert, dass die Iteration, die die Bindung unterbricht und dann fortsetzt, ungefähr fünfmal schneller läuft, als die Iteration ohne diese Optimierung.

Listing 3: Datenbindung unterbrechen und fortsetzen

private void button1_Click(object sender, EventArgs e)
{
      DataTable tbl = this.bindingSource1.DataSource as DataTable;
      Stopwatch sw1 = new Stopwatch();
      sw1.Start();
      for (int i = 0; i < 1000; i++)
      {
             tbl.Rows[0][0] = “row “ + i.ToString();
      }
      sw1.Stop();

      Stopwatch sw2 = new Stopwatch();
      sw2.Start();
      this.bindingSource1.SuspendBinding();
      for (int i = 0; i < 1000; i++)
      {
             tbl.Rows[0][0] = “suspend row “ + i.ToString();
      }
      this.bindingSource1.ResumeBinding();
      sw2.Stop();
      MessageBox.Show(String.Format(“Trial 1 {0}\r\nTrial 2 {1}“,
             sw.ElapsedMilliseconds, sw2.ElapsedMilliseconds));
}

private void Form1_Load(object sender, EventArgs e)
{
      DataTable tbl = new DataTable();
      tbl.Columns.Add(“col1“);
      tbl.Rows.Add(“one“);
      tbl.Rows.Add(“two“);
      this.bindingSource1.DataSource = tbl;
      this.textBox1.DataBindings.Add(“Text“, this.bindingSource1, “col1“);
}

Steuerelemente, die komplexe Datenbindung implementieren, wie zum Beispiel das DataGridView-Steuerelement, aktualisieren ihre Werte basierend auf Änderungsereignissen wie ListChanged, sodass sich mit SuspendBindung nicht verhindern lässt, dass diese Steuerelemente Änderungen von der Datenquelle empfangen. Diese Methoden können Sie in Szenarios mit komplexer Bindung verwenden, wenn Sie ListChanged-Ereignisse unterdrücken, indem Sie die Eigenschaft RaiseListChanged Events auf false setzen.

Layout abspecken

Änderungen wie zum Beispiel bei Größe oder Ausrichtung eines Steuerelements können ebenfalls Probleme verursachen. Fügt man zum Beispiel untergeordnete Steuerelemente in ein Formular oder ein ToolStrip ein, wird ein Control.Layout-Ereignis ausgelöst. Ändern sich Größe oder Position eines untergeordneten Steuerelements, kommt es zu einem Layout-Ereignis auf einem übergeordneten Steuerelement. Schriftgrößenänderungen bewirken ebenfalls ein Layout-Ereignis. Als Reaktion auf das Layout-Ereignis skaliert und arrangiert das Formular die Steuerelemente. Sind zu viele Layout-Ereignisse zu verarbeiten, während Sie ein Steuerelement erstellen oder in der Größe ändern, kann sich das schädlich auf die Leistung auswirken.

Komponentencode, den die Windows-Forms-Designer generieren, beginnt mit SuspendLayout und endet mit ResumeLayout. Das geschieht, um Layoutoperationen auf dem Formular zu unterdrücken, während das Formular erstellt und mit Steuerelementen gefüllt wird. Die Methode SuspendLayout erlaubt es, das mehrere Aktionen auf einem Steuerelement stattfinden, ohne dass für jede Änderung ein Layout-Ereignis ausgelöst wird. Nutzen Sie diese Technik nach Möglichkeit, um die Anzahl der Layout-Ereignisse zu minimieren.

Beachten Sie, dass SuspendLayout lediglich Layout-Ereignisse für das jeweilige Steuerelement verhindert. Wenn Sie beispielsweise Steuerelemente in ein Panel hinzufügen, müssen Sie SuspendLayout und ResumeLayout für das Panel und nicht für das übergeordnete Formular aufrufen.

Wenn sich Eigenschaften wie Bounds, Size, Location, Visible und Text bei AutoSize-Steuerelementen ändern, entstehen ebenfalls Layout-Ereignisse. Es wäre sogar noch teurer, wenn man diese Eigenschaften in Form.Load ändert, da bis dahin alle Handles erstellt werden und somit viele Nachrichten zu verarbeiten sind. Dies ist ein weiterer Platz, an dem Sie mit SuspendLayout und ResumeLayout verhindern können, dass überflüssige Layout-Ereignisse auftreten. Nehmen Sie nach Möglichkeit alle diese Änderungen innerhalb von InitializeComponent vor – somit ist überhaupt nur noch ein Layout-Ereignis notwendig.

Mehrere Eigenschaften diktieren die Größe oder Position eines Steuerelements: Width, Height, Top, Bottom, Left, Right, Size, Location und Bounds. Setzt man panel1.Width und dann panel1.Height, fällt die doppelte Arbeit an, als wenn man beide zusammen über panel1.Size festlegt. Führen Sie dazu den Code in Listing 4 aus, der den Unterschied demonstriert.

Listing 4: Die Layout-Leistung testen

private void button1_Click(object sender, EventArgs e)
{
      Stopwatch sw = new Stopwatch();
      
      sw.Start();
      for (int i = 1; i < 1000; i++) {
             panel1.Width = i;
             panel1.Height = i;
      }
      sw.Stop();

      Stopwatch sw2 = new Stopwatch();
      sw2.Start();
      for (int i = 1; i < 1000; i++) {
             panel1.Size = new Size(i, i);
      }
      sw2.Stop();
      MessageBox.Show(String.Format(“Trial 1 {0}\r\nTrial 2 {1}“,
             sw.ElapsedMilliseconds, sw2.ElapsedMilliseconds));
}

Wenn die Handles erzeugt werden, bemerken Sie den Unterschied, selbst wenn Sie SuspendLayout und ResumeLayout aufrufen. Die Methode SuspendLayout verhindert nur, das Windows-Forms-OnLayout aufgerufen wird – sie verhindert nicht, dass Nachrichten über Größenänderungen gesendet und verarbeitet werden. Setzen Sie nach Möglichkeit die Eigenschaft, die die meisten vorhandenen Informationen widerspiegelt. Wenn Sie lediglich die Größe ändern, setzen Sie Size; wenn Sie Größe und Position ändern, legen Sie Bounds fest.

Leistung beim Zeichnen

Wenn Ihre Windows-Forms-Anwendung viel zeichnen muss, profitiert sie zweifellos von erweiterter Zeichenleistung. Minimieren Sie zuerst die bei jedem Paint-Ereignis auszuführenden Arbeiten. Specken Sie Ihren Paint-Ereignishandler so weit wie möglich ab, indem Sie aufwändige Operationen herausnehmen und an eine andere Stelle verlagern. Müssen Sie beispielsweise Text, basierend auf der aktuellen ClientSize wiedergeben, können Sie das Font-Objekt mit der passenden Größe in einem Resize-Ereignishandler erstellen statt bei jedem Paint-Ereignis. Ein Brush-Objekt zum Zeichnen lässt sich auch zwischenspeichern, anstatt es wiederholt neu zu erzeugen.

Wenn Sie Ihr Steuerelement neu zeichnen möchten, können Sie seine Invalidate-Methode aufrufen. Ohne übergebene Argumente zeichnet diese Methode das gesamte Steuerelement neu. In vielen Fällen können Sie die Zeichenleistung optimieren, wenn Sie genau die Fläche berechnen, die tatsächlich neu gezeichnet werden muss, und diese Fläche als Argument an Invalidate übergeben.

Sollte dies nicht zufriedenstellend funktionieren, kommt auch die Struktur ClipRectangle infrage, die im PaintEventArgs-Parameter des OnPaint-Ereignisses eingebunden ist. Zum Beispiel können Sie mit dem folgenden Code einen Teil des Bildes zeichnen, das ungültig gemacht wurde:

Protected Overrides Sub OnPaint(_
       ByVal e As System.Windows.Forms.PaintEventArgs)

       e.Graphics.DrawImage(Me.RenderBmp, e.ClipRectangle, _
             e.ClipRectangle, GraphicsUnit.Pixel)

End Sub

Allerdings verlangt diese Technik sorgfältige Berechnungen (erst recht, wenn Bildlauf oder Skalierung beteiligt ist).

Windows Forms und die zugrunde liegende Win32-Architektur legen für jedes Steuerelement zwei Zeichenebenen offen: Hintergrund und Vordergrund. Die Funktion Control.OnPaintBackground ist für das Zeichnen der Hintergrundeffekte zuständig (normalerweise Hintergrundbild und Hintergrundfarbe) und die Funktion Control.OnPaint übernimmt das Zeichnen der Vordergrundeffekte (Bilder und Text). Wenn ihr Steuerelement weder Hintergrundbild noch Hintergrundfarbe verwendet und stattdessen alles benutzerdefiniert in OnPaint zeichnet, können Sie mithilfe des Opaque-Steuerelementstils die Leistung verbessern, indem nicht verwendete Zeichenlogik übersprungen wird. Wenn Control-Styles.Opaque im Steuerelement auf true gesetzt ist, wird die Funktion OnPaintBackground übersprungen, da angenommen wird, dass die Funktion OnPaint alle Zeichenaufgaben übernimmt (einschließlich der Hintergrunddarstellung). Zusätzlich kann dieser Stil vorteilhaft sein, wenn Ihr Steuerelement vollständig von anderen Steuerelementen verdeckt ist und sein Hintergrund ohnehin nicht gezeichnet werden muss.

Text und Bilder

Im .NET Framework 2.0 können Sie mit der TextRenderer-Klasse Text auf einem Windows-Forms-Steuerelement abmessen und zeichnen. Die Klasse TextRenderer besitzt zwei Methoden: MeasureText und DrawText mit jeweils mehreren Überladungen. Die Effizienz Ihrer Wiedergabeoperationen hängt von den gewählten Überladungen und den Optionen ab, die Sie im TextFormatFlags-Argument festlegen. Zum Abmessen einzeiliger Zeichenfolgen sollten Sie besser auf das Flag TextFormatFlag.WordBreak verzichten. Wenn dieses Flag gesetzt ist, bestimmt das GDI mit einem zuverlässigen aber aufwändigen Algorithmus die Stellen, an denen der Text umzubrechen ist. Analog sollten Sie die Optionen Text-FormatFlags.PreserveGraphicsClipping und TextFormatFlags.PreserveGraphicsTranslateTransform nicht verwenden, wenn keine Clipping-Operationen oder Transformationen auf das Graphics-Objekt angewandt wurden, weil diese Optionen einige aufwändige Berechnungen nach sich ziehen.

Nutzen Sie von TextRenderer am besten die überladenen Methoden, die kein IDeviceContext als Argument übernehmen. Diese Methoden sind effizienter, weil sie einen zwischengespeicherten bildschirmkompatiblen Speichergerätekontext verwenden, anstatt das native Handle vom internen DeviceContext abzurufen und dafür ein internes Wrapper-Objekt zu erstellen.

Zeigt Ihre Anwendung Bilddateien an und enthält außerdem eine Alpha- Komponente (für die Transparenz), können Sie die Leistung beträchtlich steigern, indem Sie die Bilder vorab in ein spezielles vormultipliziertes Bitmapformat bringen. Besitzt ein Bild eine Alpha-Komponente, sind seine Farbkomponenten mit dem Alpha-Wert zu multiplizieren, wenn das Bild angezeigt wird. Die Vorauswiedergabe eines Bildes mit vormultiplizierten Alpha-Werten eliminiert mehrere Multiplikationen (drei bei RGB-Bildern) für jedes Pixel im Bild.

Um diese Technik zu verwenden, laden Sie zuerst das Bild aus einer Datei und geben es dann mit PixelFormat.Format32bpp-PArgb in eine Bitmap wieder. Der Code in Listing 5 setzt nach diesem Verfahren das Hintergrundbild auf ein Steuerelement.

Listing 5: Ein Hintergrundbild wiedergeben

Public Overrides Property BackgroundImage() As System.Drawing.Image
      Get
             Return Me.RenderBmp
      End Get
      Set(ByVal value As System.Drawing.Image)
             Me.RenderBmp = New Bitmap(Me.Width, Me.Height, _
                    Imaging.PixelFormat.Format32bppPArgb)
             Dim g As Graphics = Graphics.FromImage(Me.RenderBmp)
             g.InterpolationMode = Drawing.Drawing2D.InterpolationMode.High
             g.DrawImage(value, New Rectangle(0, 0, Me.RenderBmp.Width, _
                    Me.RenderBmp.Height))
             g.Dispose()
      End Set

End Property

In Windows Forms 2.0 besitzt die Eigenschaft BackgroundImage eine begleitende Eigenschaft namens BackgroundImageLayout. In früheren Versionen von .NET Framework wurde das Hintergrundbild automatisch durch Windows Forms gekachelt, wodurch sich das Bild wiederholt über den Clientbereich darstellen ließ. Die neue Eigenschaft BackgroundImageLayout erlaubt es, das Layout des Hintergrundbildes auf Tile, Center, Stretch oder Zoom festzulegen. Um Anwendungskompatibilität mit vorherigen Versionen von .NET Framework zu wahren, gilt Tile als Layoutstandardeinstellung.

Neben der Ergänzung mit einem umfangreichen Satz von Features für Hintergrundbilder in Windows Forms, verbessern die Nicht-Standardwerte von BackgroundImageLayout die Leistung beim Zeichnen der Hintergrundmuster. Das Erstellen von TextureBrush ist aufwändig, da es das Abtasten des gesamten Bildes verlangt. Andere Layouts nutzen einfach die Methode Graphics.DrawImage, was in wesentlich besserer Leistung resultiert.

Für alle Layouts außer Tile kann das Einstellen der Eigenschaften BackgroundImage und BackgroundImageLayout automatisch die Eigenschaft DoubleBuffered aktivieren. Vor allem wenn das Bild eine gewisse Transparenz aufweist (wie sie über ImageFlagsHasAlpha erkannt wird), beginnt das Steuerelement, mit doppelter Pufferung zu arbeiten, um die Leistung zu erhöhen. Für das Layout Tile wird die Eigenschaft DoubleBuffered zwar nicht automatisch gesetzt, lässt sich aber manuell aktivieren.

Doppelte Pufferung ist eine Technik, die das Zeichnen beschleunigt und ein ruhigeres Bild hervorbringt, weil Flimmerscheinungen reduziert werden. Die Grundidee besteht darin, die für das Zeichnen verwendeten Operationen in einem nicht für die Bildschirmdarstellung verwendeten (sekundären) Puffer durchzuführen. Nachdem alle Zeichenoperationen abgeschlossen sind, wird dieser Puffer als ganzes Bild auf das Steuerelement gezeichnet. Dadurch verringert sich normalerweise das Flimmern und die Anwendung scheint schneller zu sein. In .NET Framework 2.0 lässt sich dieses Verhalten erreichen, indem man einen Steuerelementstil auf OptimizedDoubleBuffer setzt, was äquivalent ist mit dem Setzen der Stile DoubleBuffer und UserPaint in vorherigen Versionen des Frameworks.

Um die doppelte Pufferung vollständig zu aktivieren, müssen Sie auch AllPaintingInWmPaint auf true setzen. Ist diese Eigenschaft gesetzt, wird die Fensternachricht WM_ERASEBKGRND ignoriert und sowohl die Methode OnPaintBackground als auch die Methode OnPaint werden direkt von der WM_PAINT-Nachricht aufgerufen. Wenn Sie DoubleBuffering und AllPaintingInWmPaint auf true setzen, werden OnPaintBackground und OnPaint mit demselben gepufferten Grafikobjekt aufgerufen und alles zusammen wird off-screen gezeichnet und auf einen Schlag aktualisiert.

Um von der doppelten Pufferung zu profitieren, müssen Sie diese beiden Stile oder die Eigenschaft DoubleBuffered auf Ihrem Steuerelement auf true setzen, wie es im folgenden Code zu sehen ist:

SetStyle(ControlStyles.OptimizedDoubleBuffer |
      ControlStyles.AllPaintingInWmPaint, true);
// ODER
DoubleBufferred = true; // sets both flags

Beachten Sie, dass das Setzen der DoubleBufferd-Eigenschaft nicht immer die optimale Lösung für Zeichenprobleme ist. Verwenden Sie diese Eigenschaft abhängig von Ihrer Anwendung und den vorgesehenen Szenarios mit Vorsicht. Nachteilig beim Setzen von DoubleBuffered ist vor allem, dass für das Zeichnen sehr viel Speicher reserviert werden muss. Wenn Sie doppelte Pufferung verwenden müssen, sollten Sie das Steuerelement nicht als Ganzes ungültig machen, damit nur die wichtigsten Teile neu gezeichnet werden müssen.

Ein anderer Nebeneffekt beim Setzen von AllPaintingInWmPaint auf true kann eine sichtbare weiße Spur über den untergeordneten Steuerelementen Ihres Steuerelements sein, wenn ein Fenster, das zu einer anderen Anwendung gehört, über Ihr Fenster geschoben wird. Dies kann passieren, weil Fenster untergeordneter Steuerelemente viele WM_ERASEBKGND-Nachrichten und nur eine WM_PAINT-Nachricht erhalten. Da WM_ERASE-BKGND-Nachrichten ignoriert werden, findet für diese Fenster das Neuzeichnen zu selten statt.

Zeichentechniken anwenden

Der Bildschirm in Abbildung 1 demonstriert die Animation von Text über einem Benutzersteuerelement, für das ein Hintergrundbild festgelegt ist. Sie können unterschiedliche Zeichenoptionen festlegen, um deren Wirkung auf die Feinzeichnung und die Geschwindigkeit zu verfolgen (diese Beispielanwendung können Sie von der MSDN-Magazine-Website herunterladen).

Beispielanwendung zum Testen der Leistung bei Zeichenoperationen
Abb. 1: Beispielanwendungzum Testen derLeistung beiZeichenoperationen

Das Hauptformular enthält ein Benutzersteuerelement namensDrawingSurface mit einem Hintergrundbild, etwas Text (standardmäßig„Hello World!“) und zwei Timern. Der erste Timer steuertdie Animationsgeschwindigkeit und ist standardmäßig auf einIntervall von 10 ms gesetzt. Diesen Wert können Sie über die EinstellungInterval ändern. Am Ende jedes Intervalls wird der Text aneiner anderen Position auf der Zeichenoberfläche neu gezeichnet.Je nach gewähltem Animationseffekt bewegt sich der Text entwederim Zick-Zack über den Bildschirm („Bounce“) oder dreht sich(„Spin“).

Der zweite Timer dient dazu, die tatsächliche Animationsgeschwindigkeit zu aktualisieren, die in der unteren rechten Ecke der Zeichenoberfläche zu sehen ist. Mit tatsächlicher Animationsgeschwindigkeit ist die Anzahl der Zeichenereignisse, die in der vorherigen Sekunde verarbeitet wurden, gemeint. Die tatsächliche Animationsgeschwindigkeit kann unter der gewünschten Animationsgeschwindigkeit liegen. Zum Beispiel bedeutet ein 10-ms-Intervall, dass der Text seine Position 100-mal je Sekunde ändern soll.

Die Optionen unter BackgroundImageLayout erlauben es, die Zeichenleistung zwischen Tile und anderen Layout-Flags für das Bild zu vergleichen. Hier ist Center lediglich als Beispiel gewählt (Zoom oder Stretch hätten den gleichen Effekt wie Center). Diese Anwendung richtet das Bild neu aus, damit es sich unabhängig vom gewählten Layout an die Zeichenoberfläche anpasst. Allerdings wirkt sich das Tile-Layout auf die Leistung aus, wenn der Hintergrund gezeichnet wird. Außerdem wird die Option DoubleBuffered automatisch eingestellt, wenn das Layout Center gewählt ist, während das beim Layout Tile nicht der Fall ist.

Wie unterschiedliche Optionen die Leistung beeinflussen, können Sie erkennen, wenn Sie die Flimmereffekte beobachten und die tatsächliche Animationsgeschwindigkeit in der unteren rechten Ecke verfolgen. Die Animationsgeschwindigkeit hängt von den Zeichenoptionen, vom festgelegten Aktualisierungsintervall und von den physischen Grenzen des jeweiligen Computers ab.

Bei den durchgeführten Tests wurden die besten Ergebnisse erzielt, wenn die Optionen DoubleBufferd und Smart Invalidate aktiviert waren, Background Image Layout auf Center (oder allgemein: auf einen anderen Wert als Tile) gesetzt war und das Hintergrundbild mithilfe von PixelFormat.Format32bppPArgb im Voraus wiedergegeben wurde. Das Zeichnen ist ruhig und flimmerfrei, wenn alle diese Verfahren verwendet werden. Zudem läuft die Animation schneller ab.

Ressourcen verwalten

Der physische Speicher, den Ihre Anwendung nutzt, ist eine wichtige Leistungskenngröße. Man sollte so wenig wie möglich Speicher und Ressourcen belegen, damit so viel wie möglich für andere Prozesse übrig bleibt. Das hat nichts mit kollegialem Verhalten zu tun; Ihre Anwendung profitiert von einem kleineren Speicherbedarf und dieser Nutzen kann immens sein, wenn die Speicheraktivität so groß ist, dass der verfügbare physische Speicher verbraucht wird und der Computer zum Auslagern von Speicherseiten gezwungen ist. Doch selbst wenn Sie auf High-End-Computer abzielen und die Seitenauslagerung nicht Ihre Hauptsorge ist, sollten Sie Speicher sehr umsichtig einsetzen. Die Kosten der Speicherverwaltung in der Common Language Runtime (CLR) können für Anwendungen, die mit der Speicherzuweisung sorglos umgehen, beträchtlich sein.

Speicherbedarf, Garbage Collection und Verwaltung nativer Ressourcen wie zum Beispiel Fenster- oder GDI-Handles beeinflussen sich bei Windows-Forms-Anwendungen untereinander. Fehlende Freigabe nicht verwalteter Ressourcen erhöht den Druck auf den Garbage Collector (GC). Windows Forms versucht, die Handle-Nutzung zu verfolgen, und kann durch Aufruf von GC.Collect zusätzliche Garbage-Collection-Zyklen erzwingen, falls die Gefahr besteht, dass die Ressourcen knapp werden. Abgesehen davon müssen die verwalteten Objekte, die diese Ressourcen aufnehmen, durch den GC finalisiert werden, was teurer ist als sie eigenverantwortlich von der Anwendung über die Schnittstelle IDisposable freizugeben.

Wenn die Lebenszeit des Objekts explizit bekannt ist, sollten die unverwalteten Ressourcen, die damit verbunden sind, freigegeben werden. Wenn eine von Ihnen verwendete Klasse das Dispose-Muster implementiert und Sie explizit wissen, wann die Arbeit mit dem Objekt abgeschlossen ist, rufen Sie Dispose auf. In der Dispose-Methode rufen Sie den gleichen Aufräumcode auf, der sich im Finalizer befindet, und informieren den GC, dass er das Objekt nicht mehr durch Aufruf der Methode GC.SuppressFinalization finalisieren muss. Damit verbessert sich die Leistung, da alle nicht referenzierten Objekte in Generation null in nur einem GC-Zyklus eingesammelt werden. Wenn ein nicht mehr benötigtes Objekt finalisiert werden muss, sind mindestens zwei GC-Zyklen erforderlich, um das Objekt einzusammeln.

Objekte, die IDisposable implementieren, tun das gewöhnlich deshalb, weil sie Ressourcen aufnehmen, die deterministisch freigegeben werden sollen. Windows-Forms-Steuerelemente zum Beispiel speichern Handles, die freigegeben werden müssen. Wenn Sie Steuerelemente dynamisch in ein Formular einfügen und daraus entfernen, sollten Sie Dispose auf ihnen aufrufen – andernfalls akkumulieren Sie unerwünschte Handles in Ihrem Prozess. Sie müssen nur das oberste Steuerelement entfernen, da seine untergeordneten Elemente automatisch entfernt werden.

Wenn Sie ein Formular modal anzeigen (mithilfe der Methode ShowDialog), müssen Sie Dispose aufrufen, sobald der Benutzer das Formular geschlossen hat. Es wird in diesem Fall nicht automatisch entfernt, damit sich sein Status nach dem Schließen noch abfragen lässt. Beachten Sie, dass andere Formulare automatisch entfernt werden, sobald sie geschlossen sind.

Zwölf Performance-Tipps
  • Laden Sie in der Startphase nur wenige Module.

  • Erstellen Sie mit NGen vorkompilierte Abbilder Ihrer Assemblys.

  • Platzieren Sie stark benannte Assemblys im GAC.

  • Vermeiden Sie Kollisionen durch gleiche Basisadressen.

  • Vermeiden Sie Blockierungen im Thread der Benutzeroberfläche.

  • Realisieren Sie verzögerte (lazy) Verarbeitung.

  • Füllen Sie Steuerelemente mit schnelleren Verfahren.

  • Üben Sie mehr Kontrolle über die Datenbindung aus.

  • Verringern Sie das Neuzeichnen.

  • Verwenden Sie doppelte Pufferung.

  • Verwalten Sie die Speichernutzung.

  • Setzen Sie Reflection überlegt ein.

Grafikobjekte wie Pinsel, Stifte und Schriften implementieren ebenfalls IDisposable, da sie GDI-Handles belegen. Normalerweise erstellt man diese Objekte in der OnPaint-Methode, die jedes Mal aufgerufen wird, wenn das Neuzeichnen des Steuerelements ansteht. Wenn Sie diese Objekte nicht unverzüglich freigeben, sammeln Sie gegebenenfalls eine große Anzahl dieser Objekte an und belegen eine Menge GDI-Handles, bevor der GC diese Objekte finalisiert. Wenn Sie Ihren Code in C# oder Visual Basic schreiben, bietet sich die using-Anweisung an:

using (Pen p = new Pen(Color.Red, 1))
{
      e.Graphics.DrawLine(p, 0,0, 10,10);
}
 Using p As Pen = New Pen(Color.Red, 1)
      e.Graphics.DrawLine(p, 0,0, 10,10)
End Using

If you‘re using C++, its stack-allocation semantics are also very useful in
this regard:
Pen p(Color::Red, 1);
e->Graphics->DrawLine(%p, 0,0, 10,10);

Nach Möglichkeit sollten Sie vermeiden, GC.Collect aufzurufen. Der GC ist selbstoptimierend und passt sich selbst an die Speicheranforderungen der Anwendung an. In den meisten Fällen behindern Sie diese automatische Optimierung, wenn Sie den GC aus dem Programm heraus aktivieren. Der Aufruf von GC.Collect könnte einzig dann sinnvoll sein, wenn Sie wissen, dass Sie gerade eine Menge Speicher freigegeben haben und diesen unverzüglich einsammeln lassen wollen. Im Allgemeinen ist es aber effizienter, den GC sich selbst zu überlassen.

Umfasst Ihr Formular mehr als 200 untergeordnete Steuerelemente, sollten Sie über Möglichkeiten nachdenken, den Entwurf effizienter zu gestalten. Jedes Steuerelement verbraucht native Ressourcen und es ist recht aufwändig, wenn sehr viele Steuerelemente zu verwalten sind. Bevorzugen Sie Steuerelemente wie ListView, TreeView, DataGridView, ListBox, ToolStrip und MenuStrip. Die individuellen Elemente für diese Steuerelemente verlangen im Allgemeinen kein natives Handle.

Wenn der physische Speicher ständig belegt ist und Seitenauslagerungen auftreten, ist die physische Plattennutzung recht hoch (da Seiten auf die Festplatte geschrieben werden). Ihre Anwendung verhält sich in diesem Fall untragbar langsam und Sie werden nicht umhin kommen, die Speichernutzung zu reduzieren. Das Profiling-Tool in Visual Studio Team System kommt Ihnen hier zu Hilfe. Mit diesem Tool können Sie sich auch die Zuordnungen von verwaltetem Speicher ansehen. Auch die Profiling-Unterstützung der CLR ist nützlich (siehe hierzu msdn.microsoft.com/msdnmag/issues/05/01/CLRProfiler). Gegebenenfalls können Sie auch das Tool Virtual AddressDump (VaDump) einsetzen, um sich das Workingset (die Arbeitsseiten) des Prozesses anzusehen:

VaDump –sop <pid>

Anwendungen optimieren

Die Tabelle „Zwölf Performance-Tipps“ nennt verschiedene Wege, um die Leistung einer Anwendung zu optimieren. Alle Tipps – sowohl einzeln als auch im Verbund angewendet – bringen zweifellos Leistungsvorteile. Jetzt sollten Sie gut gerüstet sein, um schnelle und effiziente Windows-Forms-Anwendungen zu schreiben, die Ihre Benutzer beeindrucken und ihnen ein komfortables Arbeiten ermöglichen.

Der Autor

Milena Salman ist Software Design Engineer im .NET-Client- (Windows Forms)-Team bei Microsoft. Sie konzentriert sich auf die Leistungsoptimierung von Windows Forms. Milena erreichen Sie unter msalman@microsoft.com. Außerdem geht ein Dank an Shawn Burke und Jessica Fosler aus dem Windows-Forms-Team für ihren wertvollen Beitrag zu diesem Artikel.

Beispielcode Download

Anzeigen: