
Behaviors sind ein zentrales Werkzeug in der WPF-Entwicklung nach dem MVVM-Pattern. Sie kapseln UI-Logik in wiederverwendbaren Klassen, die an ein Control angehängt werden, und verhindern so, dass Logik unnötig im Code-Behind landet.
Der Beitrag zeigt zwei praxisnahe Beispiele: ein SelectAllOnFocusBehavior, das den Text einer TextBox beim Fokussieren automatisch markiert, sowie ein WatermarkBehavior, das per Adorner einen Platzhaltertext einblendet. Beide verdeutlichen typische Stolperfallen, etwa das Timing von Fokus-Events oder die verzögerte Verfügbarkeit des AdornerLayer.
Das Ergebnis: schlankerer Code-Behind, testbarere ViewModels und wiederverwendbare Logik.
In WPF-Projekten, die nach dem MVVM-Pattern implementiert wurden, kann es zu den folgenden Problemen kommen: Logik, die in das ViewModel gehört, landet im Code-Behind, weil das Event kein direktes Binding unterstützt, oder es werden Eigenschaften benötigt, die das Control nicht hergibt. Hier kommen Behaviors ins Spiel.
Ein Behavior ist eine wiederverwendbare Klasse, die an ein WPF-Control angehängt wird und dadurch dessen Verhalten erweitert. Durch Microsoft.Xaml.Behaviors.Wpf wird die Basisklasse für Behaviors (Behavior<T>) bereitgestellt.
Beschreibung: Selektiert beim Fokussieren einer TextBox automatisch den gesamten Text. Nützlich für Eingabefelder, damit der Nutzer sofort mit dem Tippen überschreiben kann, ohne erst alles manuell markieren zu müssen.
Besonderheit: Ein direkter Aufruf von SelectAll() im GotKeyboardFocus-Handler reicht nicht aus. Ein Mausklick löst zuerst GotKeyboardFocus aus und hebt die Selektion danach wieder auf. Durch Dispatcher.BeginInvoke und DispatcherPriority.Input wird sichergestellt, dass SelectAll() erst nach dem Maus-Handler ausgeführt wird.
Behavior:
1public sealed class SelectAllOnFocusBehavior : Behavior<TextBox>
2{
3 protected override void OnAttached()
4 {
5 base.OnAttached();
6 AssociatedObject.GotKeyboardFocus += OnGotKeyboardFocus;
7 }
8
9 protected override void OnDetaching()
10 {
11 AssociatedObject.GotKeyboardFocus -= OnGotKeyboardFocus;
12 base.OnDetaching();
13 }
14
15 private void OnGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
16 {
17 // Dispatcher.BeginInvoke ensures SelectAll runs AFTER the mouse-click
18 // handler that would otherwise immediately deselect the text.
19 AssociatedObject.Dispatcher.BeginInvoke(
20 DispatcherPriority.Input,
21 new Action(() => AssociatedObject.SelectAll()));
22 }
23}
XAML:
<TextBox Text="{Binding Betrag}">
<i:Interaction.Behaviors>
<behaviors:SelectAllOnFocusBehavior />
</i:Interaction.Behaviors>
</TextBox>
Beschreibung: Zeigt einen Platzhaltertext in einer leeren, nicht fokussierten TextBox an, realisiert über einen WPF-Adorner, der transparent über das Control gezeichnet wird. Der Text verschwindet, sobald der Nutzer das Feld fokussiert oder Text eingibt.
Besonderheit: Der AdornerLayer ist zum Zeitpunkt von OnAttached() noch nicht verfügbar und das visuelle Element ist noch nicht im Visual Tree verankert. Darum wartet das Behavior auf das Loaded-Event, um dort den Adorner zu registrieren.
Behavior:
1public sealed class WatermarkBehavior : Behavior<TextBox>
2{
3 // Watermark text
4 public static readonly DependencyProperty WatermarkProperty =
5 DependencyProperty.Register(
6 nameof(Watermark),
7 typeof(string),
8 typeof(WatermarkBehavior),
9 new PropertyMetadata(string.Empty, OnWatermarkPropertyChanged));
10
11 public string Watermark
12 {
13 get => (string)GetValue(WatermarkProperty);
14 set => SetValue(WatermarkProperty, value);
15 }
16
17 // WatermarkBrush
18 public static readonly DependencyProperty WatermarkBrushProperty =
19 DependencyProperty.Register(
20 nameof(WatermarkBrush),
21 typeof(Brush),
22 typeof(WatermarkBehavior),
23 new PropertyMetadata(Brushes.Gray, OnWatermarkPropertyChanged));
24
25 public Brush WatermarkBrush
26 {
27 get => (Brush)GetValue(WatermarkBrushProperty);
28 set => SetValue(WatermarkBrushProperty, value);
29 }
30
31 private WatermarkAdorner? _adorner;
32
33 private static void OnWatermarkPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
34 {
35 if (d is WatermarkBehavior behavior)
36 behavior.RefreshAdorner();
37 }
38
39 protected override void OnAttached()
40 {
41 base.OnAttached();
42
43 AssociatedObject.TextChanged += OnTextChanged;
44 AssociatedObject.GotFocus += OnFocusChanged;
45 AssociatedObject.LostFocus += OnFocusChanged;
46
47 if (AssociatedObject.IsLoaded)
48 AddAdorner();
49 else
50 AssociatedObject.Loaded += AddAdornerOnce;
51 }
52
53 protected override void OnDetaching()
54 {
55 AssociatedObject.TextChanged -= OnTextChanged;
56 AssociatedObject.GotFocus -= OnFocusChanged;
57 AssociatedObject.LostFocus -= OnFocusChanged;
58 AssociatedObject.Loaded -= AddAdornerOnce;
59
60 RemoveAdorner();
61
62 base.OnDetaching();
63 }
64
65 private void AddAdornerOnce(object sender, RoutedEventArgs e)
66 {
67 AssociatedObject.Loaded -= AddAdornerOnce;
68 AddAdorner();
69 }
70
71 private void AddAdorner()
72 {
73 var layer = AdornerLayer.GetAdornerLayer(AssociatedObject);
74 if (layer == null) return;
75
76 // remove any existing WatermarkAdorner to prevent stacking
77 var existing = layer.GetAdorners(AssociatedObject);
78 if (existing != null)
79 {
80 foreach (var a in existing)
81 {
82 if (a is WatermarkAdorner)
83 {
84 layer.Remove(a);
85 }
86 }
87 }
88
89 _adorner = new WatermarkAdorner(AssociatedObject, Watermark ?? string.Empty, WatermarkBrush ?? Brushes.Gray);
90 layer.Add(_adorner);
91 UpdateAdornerVisibility();
92 }
93
94 private void RemoveAdorner()
95 {
96 if (_adorner == null) return;
97
98 var layer = AdornerLayer.GetAdornerLayer(AssociatedObject);
99 layer?.Remove(_adorner);
100 _adorner = null;
101 }
102
103 private void RefreshAdorner()
104 {
105 if (AssociatedObject == null || !AssociatedObject.IsLoaded) return;
106 RemoveAdorner();
107 AddAdorner();
108 }
109
110 private void UpdateAdornerVisibility()
111 {
112 if (_adorner == null) return;
113
114 bool showWatermark = string.IsNullOrEmpty(AssociatedObject.Text)
115 && !AssociatedObject.IsKeyboardFocused;
116
117 _adorner.Visibility = showWatermark ? Visibility.Visible : Visibility.Collapsed;
118 }
119
120 private void OnTextChanged(object sender, TextChangedEventArgs e)
121 => UpdateAdornerVisibility();
122
123 private void OnFocusChanged(object sender, RoutedEventArgs e)
124 => UpdateAdornerVisibility();
125}
Adorner:
1internal sealed class WatermarkAdorner : Adorner
2{
3 private readonly string _text;
4 private readonly Brush _brush;
5 public WatermarkAdorner(UIElement adornedElement, string text, Brush brush)
6 : base(adornedElement)
7
8 {
9 _text = text;
10 _brush = brush;
11 IsHitTestVisible = false;
12 }
13
14 protected override Size MeasureOverride(Size constraint)
15
16 {
17
18 return constraint;
19 }
20
21 protected override Size ArrangeOverride(Size finalSize)
22
23 {
24 return finalSize;
25
26 }
27 protected override void OnRender(DrawingContext drawingContext)
28
29 {
30 var adornedElement = AdornedElement as TextBox;
31 if (adornedElement == null) return;
32
33 var typeface = new Typeface("Open Sans, Arial");
34 var fontSize = adornedElement.FontSize > 0 ? adornedElement.FontSize : 14.0;
35 var pixelsPerDip = VisualTreeHelper.GetDpi(this).PixelsPerDip;
36
37
38 var formattedText = new FormattedText(
39 _text,
40 CultureInfo.CurrentCulture,
41 FlowDirection.LeftToRight,
42 typeface,
43 fontSize,
44 _brush,
45 pixelsPerDip);
46
47 double paddingX = 4.0;
48 double availableHeight = adornedElement.ActualHeight;
49 double y = (availableHeight - formattedText.Height) / 2.0;
50 drawingContext.DrawText(formattedText, new Point(paddingX, y));
51 }
52}
XAML:
<TextBox>
<i:Interaction.Behaviors>
<bh:WatermarkBehavior Watermark="E-Mail-Adresse" />
</i:Interaction.Behaviors>
</TextBox>
Behaviors sind ein wichtiges Werkzeug in WPF-MVVM. Sie lassen sich für unterschiedliche Use Cases verwenden – von simpler UX-Verbesserung über die Umgehung von Einschränkungen bis hin zu generischer Infrastruktur.
Durch Behaviors wird der Code-Behind schlanker, das ViewModel testbarer und die Logik wiederverwendbar, was für MVVM spricht.

Praxiswissen, Tipps und Neuigkeiten für den Mittelstand: kompakt und ohne Spam. Abmelden jederzeit möglich.
Praxiswissen, Tipps und Neuigkeiten für den Mittelstand: kompakt und ohne Spam. Abmelden jederzeit möglich.