Das Problem
In der WPF scheint es eine kleine Inkonsistenz bezüglich des Verhaltens verdeckter Komponenten zu geben: überdeckt ein heruntergeklapptes Menü ein anderes Control, z.B. einen Button, so kann man ihn trotzdem anklicken.
Klickt man an dieser Stelle den Change-Background-Knopf, wird sich der Hintergrund ändern.
Etwas anders reagiert die Oberfläche, wenn die Auswahl einer Combobox den Button überdeckt:
Wenn man hier den Button drückt, wird sich die Combobox schließen, es ist ein erneuter Klick notwendig, um eine Reaktion zu erzielen.
Ansätze
Ein kurzer Blick in die ComboBox.cs offenbart das Problem: das Öffnen der Selektionsbox fängt die Maus mittels Mouse.Capture. Beim erneuten Click wird die Box geschlossen und das Event wird als behandelt markiert, die Maus wieder freigegeben.
Was tun? Ein erster Ansatz wäre natürlich eine eigene Variante der Combobox, die dieses Verhalten irgendwie unterbindet. Was nicht gerade dem Ansatz der WPF entspricht: es wäre ein Control, was genau das selbe macht wie ein anderes, und sich nur in einem bestimmten Verhalten unterscheidet. An dieser Stelle sollte es *Klick* machen: Behaviors gab es doch an anderer Stelle schon einmal, nämlich in Microsoft Expression Blend. Sie werden an Controls attached, um ihr Verhalten zu beeinflussen. Das sollte also der richtige Weg sein.
Umsetzung
Nach dem der Weg klar ist, ist die Hülle schnell geschrieben. Im Xaml die entsprechende Combobox mit dem Verhalten ausstatten
<Window x:Class="TapThroughPopup.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" xmlns:local="clr-namespace:TapThroughPopup" ... <ComboBox Width="200" HorizontalAlignment="Left" SelectedIndex="0"> <i:Interaction.Behaviors> <local:TapThroughBehavior/> </i:Interaction.Behaviors> <ComboBoxItem Content="Element 1" /> ...
und die Hülle dafür schreiben
namespace TapThroughPopup { class TapThroughBehavior : Behavior<ComboBox> { protected override void OnAttached() { } } }
Aber was genau gilt es nun zu tun? Mein erster Versuch war, das Mausevent abzufangen, es zu Verdoppeln und dem Dialog erneut zur Verarbeitung anzubieten.
Zum Beispiel:
protected override void OnAttached() { AssociatedObject.PreviewMouseDown += PreviewMouseDown; } void PreviewMouseDown(object sender, MouseButtonEventArgs e) { if (!AssociatedObject.IsDropDownOpen) return; var parent = AssociatedObject.Parent as UIElement; if (parent == null) return; parent.RaiseEvent(new RoutedEventArgs(Button.ClickEvent, this)); }
Aber Button.ClickEvent spielte ja nur darauf ab, daß ich im Beispiel einen Button klicken wollte. Sinnvoller wäre ein neues MouseButtonDown-Event (was mir je nach Klick in den Dialog eine Exception einbrachte).
Ein kurzes Innehalten – kann das so überhaupt funktionieren? Die Maus ist ja immer noch gecaptured, jedes Event, das ich generiere, müsste immer noch von der Combobox abgefangen werden.
Lösung
Letztendlich war der entscheidende Hinweis, daß man aus dem Behavior die Maus wieder freigeben kann, und so das ursprüngliche Event durchgereicht werden kann. Was mich vom zeitlichen Ablauf der Ereignis-Kette immer noch verwundert. Es scheint daran zu liegen, daß die PreviewMouseDownEvents innerhalb des Element-Trees zunächst getunnelt werden, in diesem Ablauf die Freigabe der Maus erfolgt, und erst dann die MouseDownEvents wieder nach oben bubbeln, wodurch der Click weitergereicht werden kann.
Bleibt noch das Problem zu erkennen, wann die Maus außerhalb des Bereiches der Combobox geklickt wird. Man könnte die entsprechende Funktionalität vielleicht in obigen PreviewMouseDown-Handler stecken, müsste dann aber nachsehen, ob überhaupt das DropDown geöffnet wurde, ob die Maus außerhalb des erlaubten Bereichs war… es sollte doch einen einfacheren Weg geben. Den gibt es, er trägt den unscheinbaren Namen Mouse.AddPreviewMouseDownOutsideCapturedElementHandler. Danke an dieser Stelle an den entsprechenden Eintrag in StackOverflow.
Wie so oft: wenn man erst mal weiß, wie es geht, ist es einfach. Am Ende übrig bleibt ein Zweizeiler:
using System.Windows.Controls; using System.Windows.Input; using System.Windows.Interactivity; namespace TapThroughPopup { class TapThroughBehavior : Behavior<ComboBox> { protected override void OnAttached() { Mouse.AddPreviewMouseDownOutsideCapturedElementHandler(AssociatedObject, MouseDownOutsideCapturedElement); } void MouseDownOutsideCapturedElement(object sender, MouseButtonEventArgs e) { AssociatedObject.ReleaseMouseCapture(); } } }