OnPropertyChanged mit Watermark in TextBox

Heute musste ich mal wieder an einer WPF-Anwendung arbeiten und dabei ein Formular mit recht vielen Textboxen erstellen. Eigentlich müsste man in einem solchen Fall jeder Textbox auch eine Beschriftung geben, um dem Benutzer mitteilen zu können, was an dieser Stelle einzutragen ist. Leider ist das häufig nicht sonderlich schön und übersichtlich. Außerdem benötigt man so wesentlich mehr Controls. Deshalb nutze ich gerne sogenannte Watermarks.

Textbox mit WatermarkWatermarks sind – wie man es vermutlich schon erwartet hat – leicht transparente Beschriftungen. In der Textbox befindet sich also bereits ein Text, der dem Benutzer den Sinn der Textbox mitteilt. Da der Text leicht transparent dargestellt ist, lässt er sich von tatsächlichen Benutzereingaben gut unterscheiden. Außerdem verschwindet er, sobald eine Benutzereingabe erfolgt und taucht wieder auf, wenn die Benutzereingabe entfernt wurde. So ist eine zusätzliche Beschriftung der Formularfelder nicht mehr nötig.

Wo ist das Problem?

Textboxen in WPF besitzen keine Eigenschaft für Watermarks. Das ist nicht nur sehr schade, sondern stellt manche Entwickler auch vor Probleme. Wer nicht auf diese Eigenschaft verzichten möchte oder kann, der hat nun – grob zusammengefasst – drei Möglichkeiten:

  1. Die Nutzung des WPF Toolkit
  2. Eine Alternative über drei Zeilen
  3. Eine Eigenkreation

Da ich möglichst unabhängig von Lizenzen sein möchte und nicht gewillt bin für solch eine simple Eigenschaft drei Zeilen Code zu schreiben (man bedenke, dass jede Zeile gewartet sein möchte), habe ich mich für die Eigenkreation unter Zuhilfenahme einiger Beispiele entschieden.

Der Beitrag Watermark / hint text TextBox in WPF auf Stackoverflow bot dabei ein paar nette Anregungen, aus denen ich anschließend meine eigene Textbox mit Watermark erstellt habe. Die Funktionsweise und Nutzung dieser Textbox möchte ich im Folgenden erläutern. Was meine Textbox jedoch von den Beispielen auf Stackoverflow unterscheidet, ist die Möglichkeit, bestehende Bindings zu nutzen, um vordefinierte Inhalte zu laden.

In meinem Fall wird das Formular nicht nur zum Hinzufügen neuer Daten, sondern auch zum Bearbeiten existierender Daten genutzt. Daher kommt es vor, dass das Formular geöffnet und im ViewModel (es wird das MVVM-Pattern verwendet) der Inhalt gesetzt wird. Durch das Auslösen des PropertyChanged-Events wird der Inhalt dann auch dem Benutzer sichtbar. Da die Beispiele auf Stackoverflow jedoch die Bindings entfernen, lediglich als eine Art Backup sichern und erst nach dem Event OnGotFocus wieder setzen, geht das PropertyChanged-Event verloren und es bleibt das Wasserzeichen sichtbar, bis die Textbox den Fokus hat. Damit konnte ich nicht arbeiten, weshalb ich hier ein zusätzliches Feature implementiert habe.

Aufbau der Textbox mit Watermark

Nachfolgend ist der Aufbau der Klasse TextBoxWatermarked, die von der Klasse TextBox erbt, dargestellt. Dafür wird natürlich System.Windows.Controls benötigt. Sofort macht sich der erste Unterschied zu den meisten Beispielen deutlich. Ich benötige für meine Variante zwei DependencyProperties. Das erste DependencyProperty verwende ich für das Wasserzeichen und das zweite ist meine Sicherung für das ursprüngliche Binding, wodurch ich trotz des Wasserzeichens noch Änderungen am Property erkennen kann. Auf die Methode OnOldTextChanged() werde ich später noch eingehen.

public class TextBoxWatermarked : TextBox
{
  public static DependencyProperty WatermarkProperty = 
    DependencyProperty.Register("Watermark",
    typeof(string),
    typeof(TextBoxWatermarked),
    new PropertyMetadata(new PropertyChangedCallback(OnWatermarkChanged)));

  public static DependencyProperty OldTextProperty = 
    DependencyProperty.Register("OldText", 
    typeof(string), 
    typeof(TextBoxWatermarked), 
    new PropertyMetadata(new PropertyChangedCallback(OnOldTextChanged)));
}

Die Klasse beinhaltet ein Field _isWatermarked, das später verwended wird, um festzuhalten, ob ein Text für Watermark angegeben wurde. Außerdem enthält die Klasse das Field _textBinding vom Typ Binding, das bereits in den Beispielen vorkommt und zur Sicherung des ursprünglichen Bindings verwendet wird.

Da ich an dieser Stelle nicht mit Transparenzen arbeite, sondern einfach nur eine andere Schriftfarbe nutze, wird zusätzlich das Field Foreground vom Typ Brush benötigt. Die beiden Properties Watermark und OldText sollten nun selbsterklärend sein.

private bool _isWatermarked;
private Binding _textBinding;

protected new Brush Foreground
{
  get { return base.Foreground; }
  set { base.Foreground = value; }
}

public string Watermark
{
  get { return (string)GetValue(WatermarkProperty); }
  set { SetValue(WatermarkProperty, value); }
}

public string OldText
{
  get { return (string)GetValue(OldTextProperty); }
  set { SetValue(OldTextProperty, value); }
}

Die Methode OnGotFocus() wird aufgerufen, sobald die Textbox den Fokus hat und da in diesem Fall das Wasserzeichen verschwinden soll, wird an dieser Stelle HideWatermark() aufgerufen. Verliert die Textbox den Fokus, so wird OnLostFocus() aufgerufen und damit auch ShowWatermark(). Was genau die beiden Methoden HideWatermark() und ShowWatermark() machen werde ich später noch erläutern.

protected override void OnGotFocus(RoutedEventArgs e)
{
  base.OnGotFocus(e);
  HideWatermark();
}

protected override void OnLostFocus(RoutedEventArgs e)
{
  base.OnLostFocus(e);
  ShowWatermark();
}

Im Konstruktor wird festgelegt, dass ShowWatermark() aufgerufen werden soll, sobald das Element vollständig geladen wurde. In der Methode OnWatermarkChanged() wird ebenfalls ShowWatermark() aufgerufen, um das neue Wasserzeichen anzuzeigen.

public TextBoxWatermarked()
{
  Loaded += (s, ea) => ShowWatermark();
}

private static void OnWatermarkChanged(DependencyObject sender, 
  DependencyPropertyChangedEventArgs ea)
{
  TextBoxWatermarked t = (TextBoxWatermarked)sender;
  if (t != null)
    t.ShowWatermark();
}

Nun kommen wir zu den drei wichtigsten Methoden. In der Methode ShowWatermark() wird das Wasserzeichen angezeigt. Dafür wird zunächst geprüft, ob ein Text für Watermark angegeben wurde. Ist dies der Fall, wird die zuvor erwähnte Variable _isWatermarked auf true gesetzt. Das Binding des TextPoperty wird in _textBinding gesichert und anschließend entfernt. Das ist der entscheidende Punkt, weshalb an dieser Stelle kein PropertyChanged-Event mehr funkionieren kann. Nun wird die Schriftfarbe entsprechend der gewünschten Farbe des Wasserzeichens geändert. Außerdem wird der Text an den Text des Watermark-Property angepasst. Im Anschluss wird geprüft, ob ein Binding existiert hat und falls dem so ist, wird das Binding nun auf das OldTextProperty gesetzt. Damit geht das PropertyChanged-Event nun nicht verloren. Wie in dem DependencyProperty angegeben, wird die Methode OnOldTextChanged() aufgerufen, die ich als nächstes erläutern möchte.

private void ShowWatermark()
{
  if (String.IsNullOrEmpty(Text) && !String.IsNullOrEmpty(Watermark))
  {
    _isWatermarked = true;
    _textBinding = BindingOperations.GetBinding(this, TextProperty);
    BindingOperations.ClearBinding(this, TextProperty);
    Foreground = new SolidColorBrush(Colors.Gray);
    Text = Watermark;
    if (_textBinding != null)
      BindingOperations.SetBinding(this, OldTextProperty, _textBinding);
  }
}

In der Methode OnOldTextChanged wird geprüft, ob der Text einen Inhalt hat. Ist dies der Fall, so wird auch hier die Methode HideWatermark() aufgerufen. Damit können wir das Event PropertyChanged auch dann empfangen, wenn noch das Wasserzeichen in der Textbox angezeigt wird und das Wasserzeichen entfernen.

private static void OnOldTextChanged(DependencyObject sender, 
  DependencyPropertyChangedEventArgs e)
{
  TextBoxWatermarked t = sender as TextBoxWatermarked;
  if (t != null & !String.IsNullOrEmpty(t.OldText))
  {
    t.HideWatermark();
  }
}

Als letztes wird noch die Methode HideWatermark() benötigt. In dieser Methode wird zunächst geprüft, ob ein Wasserzeichen gesetzt ist. Falls nicht, unternehmen wir an dieser Stelle gar nichts, schließlich könnten wir sonst Benutzereingaben entfernen. Ist ein Wasserzeichen gesetzt, so setzen wir die Variable _isWatermarked zunächst auf false, schließlich entfernen wir sie gerade. Anschließend setzen wir die Schriftfarbe zurück und entfernen den Text, der ja bisher auf den Text des Wasserzeichen gesetzt war. Am Ende setzen wir auch das Binding des TextProperty zurück auf das zuvor gesicherte Binding. Damit ist das Wasserzeichen entfernt und die Textbox kann mit Benutzereingaben gefüllt werden.

private void HideWatermark()
{
  if (_isWatermarked)
  {
    _isWatermarked = false;
    ClearValue(ForegroundProperty);
    Text = "";
    if (_textBinding != null) SetBinding(TextProperty, _textBinding);
  }
}

Verwendet wird die Textbox nun wie im folgenden Beispiel. Im Window-Tag muss natürlich der Namespace der Textbox angegeben werden. Anschließend kann eine Textbox definiert werden. Darin kann dann das Property Watermark zum setzen eines Wasserzeichens verwendet werden.

<Window ...
xmlns:tbw="clr-namespace:TextBoxWatermarked"
...
>

<tbw:TextBoxWatermarked Watermark="Dies ist ein Wasserzeichen" 
  Text="{Binding Path=Benutzereingabe, Mode=TwoWay, 
    UpdateSourceTrigger=PropertyChanged}" />

Damit ist das neue Feature Watermark auch schon nutzbar und verschönert hoffentlich so einige Formulare. Über nützliche Tipps und Anregungen freue ich mich natürlich wie immer.