Startseite  Inhaltsverzeichnis  <<  >>  

Kapitel 2 - Das Programmiermodell der WPF

2.1 Einführung

Dieses einleitende Kapitel erläutert die grundlegende Vorgehensweise bei der Anwendungsentwicklung mit der WPF. Es werden die verschiedenen Anwendungstypen vorgestellt, einige Änderungen gegenüber Windows Forms vorgestellt und Standardaufgaben rund um das Starten, Beenden und Konfigurieren einer Anwendung erläutert.
2.1.1 Namespaces
Die wichtigsten Typen der WPF befinden sich in den Assemblies PresentationCore.dll, PresentationFramework.dll sowie WindowsBase.dll. Diese werden in der Regel automatisch (neben einigen weiteren Assemblies) beim Erstellen eines neuen WPF-Projekts im Visual Studio referenziert. Die WPF-spezifischen Namespaces finden Sie im Namespace System.Windows und seinen untergeordneten Namespaces. Achten Sie darauf, dass viele Typen ein Äquivalent im Namespace System.Windows.Forms besitzen, z.B. wenn Sie mit der MSDN-Hilfe arbeiten.
2.1.2 Anwendungstypen
Bei den Anwendungstypen soll zwischen WPF-Anwendungstypen und den Typen, die beim Erstellen eines neuen Projekts im Visual Studio erzeugt werden können unterschieden werden. Beide überschneiden sich aber etwas. Die WPF stellt die folgenden drei Anwendungstypen bereit: Während die ersten beiden Varianten typische grafische Anwendungen sind die auf MSIL-Code basieren, lässt sich Loose XAML direkt im Browser ausführen, ohne das eine Übersetzung dazu notwendig wäre.
Übersicht der Anwendungstypen im Visual Studio
Nach der Installation der Visual Studio Extensions des .NET Frameworks 3.0 in das Visual Studio 2005 stehen dort neue Projekttypen zur Verfügung, die Sie über FILE - NEW - PROJECT verwenden können.

WPF-Projekttypen im Visual Studio

Abbildung 2.1: WPF-Projekttypen im Visual Studio

AnwendungstypBeschreibung
Windows-Anwendung
Windows Application (WPF)
Hierbei handelt es sich um eine typische Windows-basierte Anwendung, die allerdings auf den Komponenten und Möglichkeiten der WPF basiert.
WebBrowser-Anwendung
XAML Browser Application (WPF)
Hier handelt es sich um "echte" Anwendungen, die allerdings im Browser ausgeführt werden und mit Internet-Rechten ausgestattet sind (d.h. weniger Befugnisse besitzen).
Steuerelementbibliothek
Custom Control Library (WPF)
In dieser Bibliothek werden Steuerelemente speziell für WPF-Anwendungen hinterlegt.

Tabelle 2.1: Übersicht der Anwendungsypenim Visual Studio

nach oben

2.2 Aufbau eines Projekts

Für Liebhaber der Konsole und geduldige Entwickler bietet sich die Entwicklung von WPF-Anwendungen rein über die Tools des .NET Frameworks wie "MSBuild" sowie dem Windows SDK und den WPF-Komponenten an. Eine professionelle und schnellere Entwicklung ist dagegen nur mit einer Entwicklungsumgebung wie dem Visual Studio möglich. Aus diesem Grund wird das Visual Studio auch durchgängig in diesem Buch verwendet. Aber auch in diesem Fall ist es nützlich, den Grundaufbau eines Projekts zu kennen und zu wissen, welche Dinge automatisch an welcher Stelle generiert werden.

Die verschiedenen Anwendungstypen wurden ja bereits vorgestellt. Allerdings gibt es unterschiedliche Varianten, eine solche Anwendung zu erstellen bzw. um überhaupt etwas zu erstellen, was irgendwie genutzt werden kann. Es sind folgende Vorgehensweisen möglich:

HINWEIS: Innerhalb dieser drei Anwendungstypen werden jetzt die Bestandteile eines WPF-Projekts vorgestellt. Lesen Sie deshalb auch alle drei Varianten einmal durch.

2.2.1 Reine Code-Anwendungen
Auch dieser Anwendungstyp hat seine Berechtigung. Wenn Sie keine grafische Oberfläche benötigen (in einer WPF-Anwendung ist das natürlich irgendwie witzlos), diese dynamisch generieren müssen oder einfach auf XAML verzichten wollen um nicht noch eine Sprache bzw. Syntax erlernen zu müssen, dann können Sie eine Anwendung auch rein im Code erzeugen. Sie haben im Code immer alle Ausdrucksmöglichkeiten zur Verfügung, während XAML einige Einschränkungen besitzt.

Wie schon seit der ersten Version des .NET Frameworks üblich wird dazu ein Application-Objekt benötigt, welches aus dem Namespace System.Windows stammt. Statt einem Form- wird nun allerdings ein Window-Objekt erzeugt, um ein Fenster anzuzeigen. Der Rest sollte von Windows Forms-Anwendungen her bekannt sein. Neben den zum Teil anderen Klassen müssen Sie natürlich auch andere Namespaces verwenden.

Obwohl diese Vorgehensweise relativ einfach und bekannt aussieht soll hier gleich darauf hingewiesen werden, dass es bei der Verwendung und Konfiguration der WPF-Komponenten etwas komplizierter wird, zumindest anfangs.

BEISPIEL: MinimalCodeProj - Download

Erstellen Sie im Visual Studio ein Windows Application (WPF)-Projekt und entfernen Sie die beiden Dateien App.xaml und Window1.xaml aus dem Projekt im Projektmappenexplorer. Fügen Sie dem Projekt dann eine Klasse über den Kontextmenüpunkt ADD - NEW ITEM des Projekts hinzu. Jetzt kann es mit der Eingabe des Codes weitergehen.

Zuerst einmal müssen die benötigten Namespaces eingebunden werden. Der Namespace System.Windows stellt die Window- sowie die Application-Klasse bereit. Um den Hintergrund des Fensters einzufärben, wird ein Brush (Pinsel) benötigt. Diese Klasse befindet sich im Namespace System.Windows.Media. In der auch in WPF-Anwendungen benötigten Main()-Methode (wenn es auch Varianten gibt, in der Main() nicht explizit angegeben werden muss, z.B. in Anwendungen die XAML nutzen) wird ein Application-Objekt erstellt und mit dem Aufruf von Run() die Verarbeitung der Nachrichtenschleife gestartet (über welche die Anwendung Nachrichten verarbeitet wie das Klicken einer Schaltfläche oder die Eingabe von Text in ein Textfeld). In der Methode Run() wird dann eine Instanz des Hauptformulars übergeben. Am Ende der Verarbeitung in Run() wird das Ereignis Startup ausgelöst, in das Sie Ihren Initialisierungscode für die gesamte Anwendung unterbringen können.

Vor der Methode Main() muss über das Attribut STAThread das Single-Thread-Modell aktiviert werden. Lassen Sie diese Angabe weg, kommt es relativ zügig zu einer Exception, da dies für die korrekte Verwendung einiger UI-Komponenten zwingend notwendig ist (z.B. bei der Kommunikation mit der Zwischenablage oder den Systemdialogen, mit denen über COM Interop kommuniziert wird).

Auch die Ereignisbehandlung funktioniert wie unter .NET 2.0. Lediglich die Argumente der Ereignishandler sind etwas anders, dies wird aber später erläutert.

Das Ergebnis der Anwendung

Abbildung 2.2: Das Ergebnis der Anwendung

using System.Windows;
using System.Windows.Media;
namespace MinimalCode
{
  class FrmMain: Window
  {
    public FrmMain()
    {
      Title = "Hallo von der WPF";
      Width = 230;
      Height = 100;
      Background = Brushes.AliceBlue;

      StackPanel sp = new StackPanel();
      this.Content = sp;

      Button btn = new Button();
      btn.Content = "Klick mich";
      btn.Click += OnClick;

      sp.Children.Add(btn);
    }
    private void OnClick(object sender, RoutedEventArgs e)
    {
      MessageBox.Show("Funzt");
    }
    [System.STAThread()]
    public static void Main()
    {
      new Application().Run(new FrmMain());
    }
  }
}
Listing 2.1: Beispiele\Kap02\MinimalCodeProj\FrmMain.cs

2.2.2 Reine XAML-Anwendungen
Um eine reine XAML-Anwendung zu erstellen, können Sie wieder eine Windows-Anwendung für die WPF erzeugen und löschen aus dieser die Code-Behind-Dateien App.xaml.cs und Window1.xaml.cs für die Dateien App.xaml und Window1.xaml. Da diese Dateien keine Anwendungslogik enthalten, ist das Löschen völlig unproblematisch.

BEISPIEL: MinimalXamlProj - Download

Die letzte Anwendung wird nun nur noch mit Hilfe von XAML erzeugt. Die "Hauptanwendung" besteht aus dem XML-Element Application, welches später ein Application-Objekt erzeugt. Das Fenster wird über ein Window-Element beschrieben, in dem mehrere andere Elemente verschachtelt sind, unter anderem der angezeigte Button. Alle Eigenschaften, die Sie vorher im Code gesetzt haben, werden jetzt über Attribute in der XAML-Datei zugewiesen. Zugehörigkeit wird dabei durch Verschachtelung erreicht.

<Application x:Class="MinimalXAML.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    StartupUri="Window1.xaml">
</Application>
Listing 2.2: Beispiele\Kap02\ MinimalXamlProj\App.xaml

<Window x:Class="MinimalXamlProj.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Hallo von der WPF" Height="100" Width="230"
    Background="AliceBlue">
  <StackPanel>
    <Button Click="OnClick">Klick mich</Button>
    <x:Code>
      void OnClick(object sender, RoutedEventArgs e)
      {
      MessageBox.Show("Funzt");
      }
    </x:Code>
  </StackPanel>
</Window>
Listing 2.3: Beispiele\Kap02\ MinimalXamlProj\Window1.xaml

2.2.3 Gemischte XAML-Code-Anwendungen
In gemischten Anwendungen, die XAML-Dateien und dazugehörige Code-Dateien, so genannte Code-Behind-Dateien, enthalten, besteht in der Regel eine strikte Trennung zwischen Anwendungslogik und Darstellung. Dieses Vorgehen sollte auch die Standardvorgehensweise für Ihre WPF-Anwendungen sein. Während in der XAML-Datei die Oberfläche (ohne jeglichen Code) beschrieben wird, wird in der Code-Behind-Datei (dahinter liegende Code-Datei) die Anwendungslogik programmiert. Die Verknüpfung zwischen beiden Dateien besteht in der Verwendung von partiellen Klassen, wie Sie bereits unter .NET 2.0 eingeführt wurden. Eine Standard-WPF-Windows-Anwendung besteht aus den Anwendungsdateien App.xaml und App.xaml.cs sowie einem vordefinierten Fenster, das durch die Dateien Window1.xaml und Window1.xaml.cs erzeugt wird. Das Visual Studio stellt die Zusammengehörigkeit von XAML- und C#-Dateien auch grafisch im Projektmappenexplorer dar.

Zusammengehörige Dateien werden verschachtelt dargestellt

Abbildung 2.3: Zusammengehörige Dateien werden verschachtelt dargestellt

Nach dem Erzeugen der WPF-Windows-Anwendung erhalten Sie den folgenden unveränderten Code, der diesmal als Abbildung vorliegt, um die Verknüpfungen besser darzustellen. Der Code wurde lediglich etwas umformatiert, Kommentare und using-Anweisungen wurden entfernt.

Beziehungen zwischen den XAML- und Code-Behind-Dateien

Abbildung 2.4: Beziehungen zwischen den XAML- und Code-Behind-Dateien

2.2.4 Erstellungsprozess einer WPF-Anwendung
Auch wenn es auf den ersten Blick nicht so scheint, wird nichts vor Ihnen verborgen. Sie können den gesamten Erstellungsprozess einer WPF-Anwendung vollständig nachvollziehen. Begonnen wird dazu mit der Projektdatei *.csproj. Diese enthält ein Skript, welches direkt von MSBuild, dem mit .NET 2.0 eingeführten Buildsystem, genutzt werden kann. Der folgende Code zeigt eine verkürzte, aber voll funktionsfähige Projektdatei. Es soll hier allerdings nicht der gesamte Aufbau erläutert werden sondern nur einige interessante Stellen.

ACHTUNG: Zum Verständnis der folgenden Erläuterungen sind schon Grundkenntnisse zu XAML und WPF-Anwendungen notwendig. Der Abschnitt befindet sich der Vollständigkeit halber an dieser Stelle und ist für Interessierte gedacht, die einmal einen Blick hinter die Kulissen werfen wollen.

Der Eintrittspunkt einer Anwendung ist die Datei, welche im Element ApplicationDefinition angegeben wird, in diesem Fall App.xaml. Damit wäre schon einmal klar, wo es losgeht. Jede XAML-Datei, die ein Fenster definiert und aus einem XAML- und einem Code-Teil besteht, muss in einem Page-Element angegeben werden. Die zu kompilierenden Dateien werden in einer zweiten Gruppe über Compile-Elemente definiert. Die verschachtelten Elemente DependentUpon und SubType bei der Datei Window1.xaml sorgen dafür, dass das Visual Studio die Zusammengehörigkeit beider Dateien erkennt und diese wie in Abbildung 2.3 darstellt. Die beiden importieren *.target-Dateien, eine für die Sprache (CSharp) und eine für die WPF (WinFX) sorgen für die Übersetzungslogik. Sie finden MSBuild im Verzeichnis [WinDir]\Microsoft.NET\Framework\v2.0.50727. Im gleichen Verzeichnis befinden sich auch die *.target-Dateien. Um eine solche Projektdatei direkt mit MSBuild zu starten geben Sie auf der Kommandozeile ein msbuild <ProjektName>.csproj.

<Project DefaultTargets="Build"
  xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

  <PropertyGroup>
    <AssemblyName>MinimalXAML</AssemblyName>
    <OutputType>winexe</OutputType>
    <OutputPath>.</OutputPath>
  </PropertyGroup>

  <ItemGroup>
    <Reference Include="System" />
    <Reference Include="WindowsBase" />
    <Reference Include="PresentationCore" />
    <Reference Include="PresentationFramework" />
  </ItemGroup>

  <ItemGroup>
    <ApplicationDefinition Include="App.xaml" />
    <Page Include="Window1.xaml" />
  </ItemGroup>

  <ItemGroup>
    <Compile Include="App.xaml.cs" />
    <Compile Include="Window1.xaml.cs">
      <DependentUpon>Window1.xaml</DependentUpon>
      <SubType>Code</SubType>
    </Compile>
  </ItemGroup>

  <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets"/>
  <Import Project="$(MSBuildBinPath)\Microsoft.WinFX.targets"/>
</Project>

Listing 2.4: Verkürzte Projektdatei einer WPF-Anwendung

Die Datei App.xaml ist also der Einsprungpunkt der Anwendung. Das Application-Element bewirkt, dass später ein Application-Objekt erzeugt wird. Über das Attribut x:Class geben Sie an, dass es sich bei der generierten Klasse, die beim Übersetzen für die XAML-Datei erstellt wird, um eine partielle Klasse handelt. Als Wert wird der Namespace und der Klassename angegeben. In der Code-Behind-Datei App.xaml.cs befindet sich ebenfalls eine partielle Klasse die von Application abgeleitet ist. In ihr können Sie bei Bedarf die anwendungsspezifische Logik unterbringen.

Damit die Anwendung weis, welches Fenster sie zu Beginn erzeugen und anzeigen soll, wird in der XAML-Datei das Attribut StartupUri angegeben. Als Wert wird die XAML-Datei Window1.xaml des Hauptfensters zugewiesen. In der Datei Window1.xaml befindet sich zu Beginn wiederum ein x:Class-Attribut mit dem Namen der Klasse der Code-Behind-Datei. Auf diese Weise werden die XAML- und die Code-Behind-Dateien miteinander verknüpft. Die Code-Behind-Klasse muss dabei von der Klasse abgeleitet sein, welche in der XAML-Datei als Wurzelelement verwendet wurde. Besitzt die XAML-Datei als Wurzelelement das Element Window, dann muss auch die Klasse in der C#-Datei von Window abgeleitet werden. Allerdings kann die Ableitung auch weggelassen werden. In diesem Fall erfolgt die Ableitung automatisch.

In der Datei Window1.xaml können Sie die Benutzeroberfläche definieren, während Sie in der Datei Window1.xaml.cs die Programmlogik unterbringen. In letzterer befindet sich lediglich der Konstruktor mit dem wichtigen Aufruf von InitializeComponent().

Was vermissen Sie? Es befindet sich im Code weder eine Methode Main() noch die Methode InitializeComponent(). Beide werden während der Übersetzung generiert und in die Assembly eingebunden.

Für die Klasse App vom Typ Application der Testanwendung wird beispielsweise der folgende Code erzeugt. Um diesen herauszubekommen, wurde das überaus nützliche Tool Reflector von Lutz Roeder (http://www.aisto.com/roeder/dotnet/) verwendet.

Und hier ist die vermisste Main()-Methode

Abbildung 2.5: Und hier ist die vermisste Main()-Methode

In der Main()-Methode wird ein neues App-Objekt erzeugt, über InitializeComponent() das Startfenster definiert (vgl. folgender Code) und zum Abschluss die Nachrichtenschleife gestartet.

[DebuggerNonUserCode]
public void InitializeComponent()
{
  base.StartupUri = new Uri("Window1.xaml", UriKind.Relative);
}
Der Aufruf von InitializeComponent() ist allerdings noch nicht die Methode aus dem Konstruktor der Fensterklasse. Sie befindet sich an anderer Stelle und kann auch ohne den Reflector betrachtet werden (wie auch die eben gezeigten Sourcen). Beim Übersetzen einer WPF-Anwendung, welche auch XAML-Dateien verwendet, werden einige interessante Dateien im Verzeichnis ..\obj\Debug erzeugt. So finden Sie hier eine Datei App.g.cs die den gesamten automatisch generierten Code für die Hauptanwendung enthält, also auch die Methoden Main() und InitializeComponent().

HINWEIS: Das Ergebnis der Übersetzung einer XAML-Datei finden Sie in der Datei mit der Endung *.baml (Binary Application Markup Language). Diese enthält den XAML-Code in binärer und effizienter verarbeitbarer Form, allerdings nicht in MSIL. Dann wird sie als Ressource in die fertige Assembly eingebunden.

Ferner finden Sie unter den generierten Dateien eine Datei Window1.g.cs, welche den generierten Code für das Fenster enthält (das Zeichen g im Dateinamen steht für generated). Hier befindet sich nun auch die Methode InitializeComponent(), die im Konstruktor der Fensterklasse aufgerufen wird. Diese hat zwei Aufgaben. Zuerst wird ein Uri-Objekt für die in der Assembly eingebettete XAML-Ressource erzeugt. Danach wird diese über die Methode LoadComponent() geladen. In der Methode Connect() des Interfaces IComponentConnector werden zum Abschluss Beziehungen für die in der XAML-Datei mit einem Namen versehenen Komponenten und in der Klasse bereits vorgehaltenen Instanzvariablen hergestellt. Außerdem werden die Ereignishandler der Code-Behind-Klasse mit den Komponenten verknüpft.

namespace WindowsApplication1
{
  public partial class Window1: Window, IComponentConnector
  {
    internal TextBox Tb1;
    internal Button Btn1;
    private bool _contentLoaded;
    public void InitializeComponent()
    {
      if(_contentLoaded)
      {
        return;
      }
      _contentLoaded = true;
      Uri resourceLocater = new Uri("/WindowsApplication1;component/window1.xaml", UriKind.Relative);
      System.Windows.Application.LoadComponent(this, resourceLocater);
    }
    void IComponentConnector.Connect(int connectionId, object target)
    {
      switch(connectionId)
      {
        case 1: this.Tb1 = ((TextBox)(target));
                this.Tb1.Loaded += new RoutedEventHandler(this.OnLoad);
                return;
        case 2: this.Btn1 = ((Button)(target));
                return;
      }
      this._contentLoaded = true;
    }
  }
}

Listing 2.5: Auszug aus der generierten Datei Window1.g.cs

nach oben

2.3 Nützliche Anwendungseigenschaften

Müssen Sie einmal auf das Anwendungsobjekt der aktuellen Anwendung (konkret der aktuellen AppDomain) zugreifen, verwenden Sie die statische Eigenschaft Current der Klasse Application.
Application current = Application.Current;
Eine Anwendung beenden
Standardmäßig wird eine WPF-Anwendung beendet, wenn das letzte Fenster geschlossen wird. Dies liegt am Standardwert OnLastWindowClose der Eigenschaft ShutdownMode. Es reicht also, das Hauptfenster mit Close() zu schließen unter der Voraussetzung, dass keine weiteren Fenster geöffnet sind. Ein anderer Wert ist OnMainWindowClose. In diesem Fall wird die Anwendung beim Schließen des Hauptfensters beendet. Möchten Sie die Anwendung selbst beenden, setzen Sie den Wert OnExplicitShutdown. In diesem Fall rufen Sie die Methode Shutdown() des aktuellen Application-Objekts auf.
Application.Current.Shutdown();
Kommandozeilenparameter verarbeiten
Im Ereignis Startup wird ein Parameter vom Typ StartupEventArgs übergeben. Dieser besitzt eine Eigenschaft Args über die Sie Zugriff auf die Parameter erhalten.
private void OnStartup(object sender, StartupEventArgs e)
{
  foreach(string arg in e.Args)
    cmdArgs.Add(arg);
}

Listing 2.6: Beispiele\Kap02\ ApplicationPropsProj\App.xaml.cs

Gemeinsame Verwendung von Anwendungsinformationen
Das Application-Objekt einer Anwendung, welches über Application.Current jederzeit abgerufen werden kann, besitzt eine Aufzählung Properties, über die anwendungsweite Einstellungen verwaltet und bereitgestellt werden können.
Application.Current.Properties.Add("Autor", "Dirk Frischalowski");
TbInfo.AppendText(Application.Current.Properties["Autor"] + "\r");

Listing 2.7: Beispiele\Kap02\ApplicationPropsProj\App.xaml.cs und Window1.xaml.cs

nach oben