## 17.7 Ereignisse in der WPF ### 17.7.1 Allgemeine Grundlagen Natürlich reagiert auch eine WPF-Anwendung nur dann, wenn Ereignisse ausgelöst werden. Zunächst einmal unterscheiden sich die Ereignisse nicht von den anderen im .NET Framework, das heißt, die Ereignishandler müssen zwei Parameter aufweisen: Der erste muss vom Typ Object sein, und der zweite muss von EventArgs abgeleitet sein. Nehmen wir an, Sie möchten das Click-Ereignis einer Schaltfläche in der XAML-Datei programmieren. Sie können die IntelliSense-Unterstützung nutzen und daraus Click auswählen. Durch zweimaliges Drücken der -Taste wird im XAML-Code das Ereignis an einen Ereignishandler gebunden, der in der Code-Behind-Datei erzeugt wird.
    <Grid>
      <Button Click="button1_Click"  Height="30" Name="button1"
              Margin="70,95,133,0">Button1</Button>
    </Grid>
Hinweis
Sie können einen Ereignishandler auch im Eigenschaftsfenster bereitstellen. Dazu schalten Sie die Ansicht der Eigenschaften in die Ansicht der Ereignisse um, indem Sie auf das Symbol mit dem Blitz klicken. Ein Doppelklick auf das gewünschte Ereignis genügt, um den Ereignishandler mit der üblichen Namenskonvention zu erzeugen.
Stellt eine Komponente ein Standardereignis bereit, können Sie den Ereignishandler auch mittels Doppelklick auf die Komponente bereitstellen. Das gilt zum Beispiel auch für den Button. Die Verknüpfung zwischen Ereignis und Ereignishandler können Sie auch im Code dynamisch festlegen, z. B. so:
    public partial class MainWindow : Window {
      public MainWindow() {
        InitializeComponent();
        button1.Click += new RoutedEventHandler(button1_Click);
      }
      void button1_Click(object sender, RoutedEventArgs e) {
        MessageBox.Show("Im Click-Ereignishandler");
      }
    }
### 17.7.2 Routed Events Auch wenn es bis jetzt den Anschein hat, dass die Ereignisse in der WPF keine Besonderheiten bergen, ist dem nicht so. Der Grund ist, dass in einer WPF- Benutzeroberfläche die Elemente ineinander verschachtelt werden können, beispielsweise so:
    <Window ...>
      <StackPanel>
        <Button>
          <Canvas>
            <Ellipse></Ellipse>
          </Canvas>
        </Button>
      </StackPanel>
    </Window>
Hier enthält der Button ein Canvas-Element, das seinerseits eine Ellipse beschreibt. Klickt der Anwender auf den Button, könnte es sich dabei um das Ellipse-Element handeln, das im Button eingebettet ist. Möchten Sie jedoch sicherstellen, dass beispielsweise das Click-Ereignis der Schaltfläche auch dann ausgelöst wird, wenn der Benutzer die Ellipse trifft, hätten Sie ein kleines Problem. Jetzt helfen uns die von der WPF eingeführten Routed Events weiter, die wie folgt kategorisiert sind: Direkte Events: die Gruppe der Ereignisse, die nur von dem Element verarbeitet werden, bei dem das Ereignis aufgetreten ist. Diese Ereignisse unterscheiden sich nicht von den sonst im .NET Framework üblichen. Tunneling-Events: Beim Tunneling beginnt die Ereigniskette beim Wurzelelement. In der Regel dürfte es sich dabei um Window handeln. Dieses reicht das Ereignis immer an das nächste untergeordnete Element weiter, das seinerseits wieder das ihm untergeordnete Element informiert. Das geschieht so lange, bis der Auslöser erreicht ist. Die Tunneling-Events sind durch das Präfix Preview gekennzeichnet. Die meisten Routed Events haben ein korrespondierendes Preview-Ereignis. Bubbling-Events: Beim Bubbling von Ereignissen wird der ausgelöste Event an die übergeordnete Komponente in der Hierarchie weitergereicht. Diese kann nun ebenfalls darauf reagieren. Wird ein Ereignis ausgelöst, das nicht zu den direkten Events gerechnet wird, kommt es zu einer Abfolge von Ereignisauslösungen. Zuerst werden alle Tunneling-Events ausgelöst, wobei das Wurzelelement an der Spitze steht. Anschließend folgt das erste untergeordnete Element. Legen wir das Codefragment von oben zugrunde, würde anschließend das Ereignis im StackPanel ausgelöst, danach im Button, gefolgt vom Canvas und zuletzt im Ellipse-Element – vorausgesetzt, es wurde auf die Ellipse in der Schaltfläche geklickt. Ist die Kette der Tunneling-Events durchlaufen, geht es mit den Bubbling Events in entgegengesetzter Richtung zurück. Also zuerst mit dem Bubbling-Event in der Ellipse, danach im Canvas-Element, dem Button, dem StackPanel. Den Abschluss bildet das Window.
Hinweis
Um die getunnelten von den gebubbelten Ereignissen unterscheiden zu können, haben alle getunnelten Ereignisse das Präfix Preview.
Abbildung 17.7 verdeutlicht den Zusammenhang. Dabei wurde aber eine etwas einfachere Oberflächenstruktur zugrunde gelegt. Hier klicken, um das Bild zu vergrößern Abbildung 17.7 Abfolge der Routed Events Routed Events sind eine Spezialform von Ereignissen in der WPF. Es werden dabei alle Handler aufgerufen, die sich bei dem Ereignis registriert haben und zwischen dem Element, bei dem das Ereignis aufgetreten ist, und der Wurzel liegen. Behandelt ein Element das aufgetretene Ereignis nicht, wird die Ereigniskette nicht unterbrochen.
Anmerkung
Im Grunde genommen ist es nicht ganz richtig, bei den direkten Events von Routed Events zu sprechen. Zu den Routed Events gehören eigentlich nur die gebubbelten und getunnelten, also diejenigen, die weitergeleitet werden. Da aber auch die Dokumentation die direkten Events der Gruppe der Routed Events zuordnet, halte ich mich an diese Vorgabe.
#### Beispielprogramm Wir sollten uns nun den Ablauf der Routed Events an einem konkreten Beispiel ansehen. Das folgende Beispielprogramm definiert dazu im Window ein Grid, dem mit einem Button und einer ListBox zwei Elemente zugeordnet sind. Der Button enthält ein untergeordnetes Element vom Typ Label. Die ListBox dient ausschließlich zum Anzeigen der Ereigniskette. Alle Elemente reagieren auf das Auslösen der beiden Ereignisse PreviewMouseLeftButtonDown und MouseLeftButtonDown.
    // -----------------------------------------------------
    // Beispiel: ...\Kapitel 17\RoutedEvents
    // -----------------------------------------------------
    <Window ...
            MouseLeftButtonDown="Event_MouseDown"
            PreviewMouseLeftButtonDown="Event_PreviewMouseDown">
      <Grid MouseLeftButtonDown="Event_MouseDown"
            PreviewMouseLeftButtonDown="Event_PreviewMouseDown">
        <Grid.RowDefinitions>
          <RowDefinition Height="80"/><RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Button Grid.Row="0" Name="button1" Margin="20"
                MouseLeftButtonDown="Event_MouseDown"
                PreviewMouseLeftButtonDown="Event_PreviewMouseDown">
          <Label Name="label1" MouseLeftButtonDown="Event_MouseDown"
                 PreviewMouseLeftButtonDown="Event_PreviewMouseDown">
            Klick mich ...
          </Label>
        </Button>
      <ListBox Grid.Row="1" Name="listBox1"></ListBox>
    </Grid>
    </Window>
In der Code-Behind-Datei sind die beiden Ereignishandler definiert, die auf die Ereignisse reagieren. Je nachdem, welches Element das Ereignis ausgelöst hat, wird in die ListBox eine entsprechende Mitteilung geschrieben.
    private void Event_MouseDown(object sender, MouseButtonEventArgs e) {
      if (sender is Button)
        listBox1.Items.Add("BUTTON");
      if (sender is Label)
        listBox1.Items.Add("LABEL");
      else if (sender is Grid)
        listBox1.Items.Add("GRID");
      else if (sender is MainWindow)
        listBox1.Items.Add("WINDOW");
    }

    private void Event_PreviewMouseDown(object sender, MouseButtonEventArgs e) {
      if (sender is Button)
        listBox1.Items.Add("BUTTON-Preview");
      else if (sender is Label)
        listBox1.Items.Add("LABEL-Preview");
      else if (sender is Grid)
        listBox1.Items.Add("GRID-Preview");
      else if (sender is MainWindow)
        listBox1.Items.Add("WINDOW-Preview");
    }
Wenn Sie die Anwendung starten und auf die Beschriftung klicken, werden Sie die Ausführungen zu den Routed Events nicht 100%ig bestätigt sehen, denn der Button meldet sich nicht und auch alle nachfolgenden gebubbelten Ereignisse werden nicht mehr ausgelöst (siehe Abbildung 17.8). Hier klicken, um das Bild zu vergrößern Abbildung 17.8 Das Beispielprogramm »RoutedEvents« zur Laufzeit Der Button ist ein Steuerelement, das wegen des Click-Ereignisses eine Ausnahme darstellt. Das Click-Ereignis schluckt regelrecht alle anderen Ereignisse und bricht die Kette der getunnelten Ereignisse ab. Gelöst wird dieses Problem durch die Registrierung des allgemeinen MouseDownEvents bei dem entsprechenden Ereignishandler mit der Konstruktormethode AddHandler. Die Registrierung erfolgt im Konstruktor des Window.
    button1.AddHandler(MouseDownEvent,
            new MouseButtonEventHandler(Event_MouseDown), true);
Dieser Methode übergeben Sie zuerst den Ereignistyp und danach ein Delegate auf den Ereignishandler. Der boolesche Parameter gibt an, ob bereits behandelte Ereignisse weiterverarbeitet werden sollen. Tragen Sie hier false ein, wird – falls auf das Label geklickt wird – nur das Ereignis für das Label behandelt. Alle anderen Ereignisse werden nicht mehr registriert. Trotzdem wird die Anzeige in der ListBox immer noch nicht wie gewünscht sein. Sie müssen außerdem die Eigenschaft Cancel des MouseButtonEventArgs-Parameters auf false setzen, damit die Ereigniskette dort endet, wo sie begonnen hat:
    private void Event_MouseDown(object sender, MouseButtonEventArgs e) {
      if (sender is Button) {
        listBox1.Items.Add("BUTTON");
        e.Handled = false;
      }
      ...
    }
#### Ereignisweiterreichung abbrechen Bei einem Ereignis kann die Ereigniskette jederzeit abgebrochen werden. Dazu dient die Eigenschaft Handled des EventArgs-Parameters, der auf true gesetzt wird. Um bei der Auslösung des Preview-Ereignisses des Grids die Ereigniskette zu beenden, müsste der Code im Ereignishandler wie folgt ergänzt werden:
    private void Event_PreviewMouseDown(object sender, MouseButtonEventArgs e) {
      if (sender is Button)
        listBox1.Items.Add("BUTTON-Preview");
      else if (sender is Label)
        listBox1.Items.Add("LABEL-Preview");
      else if (sender is Grid) {
        e.Handled = true;
        listBox1.Items.Add("GRID-Preview");
      }
      else if (sender is Window1)
        listBox1.Items.Add("WINDOW-Preview");
    }