WPF Search Box custom control

The following code presents a custom WPF control that displays a search box where an empty label is displayed as a tool tip when no text is inserted. You can also set a result label if needed. More customizations will be available soon.

Preview:


SearchBox.cs (Code)

Download

////////////////////////////////////////////////////////////////////////
/// @file SearchBox.cs
///
/// @author Jonathan Schmidt <jschmidt42@gmail.com>
///
/// @pre 
///      Copyright (C) 2010 - All Rights Reserved
///      All rights reserved. http://www.equals-forty-two.com
///      
///      Redistribution and use in source and binary forms, with or 
///      without modification, and with author's authorization, are 
///      permitted provided that the following conditions are met:
///      
///      1. Redistributions of source code must retain the above 
///         copyright notice, this list of conditions and the following 
///         disclaimer.
///      
///      2. Redistributions in binary form must reproduce the above 
///         copyright notice, this list of conditions and the following 
///         disclaimer in the documentation and/or other materials 
///         provided with the distribution.
///
/// @date 2010-12-31
///
/// @brief Search box control
////////////////////////////////////////////////////////////////////////
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Globalization;
 
namespace Engine42.Editor.UI.Controls
{
  /// <summary>
  /// The search box is a text box that display a label when no text is 
  /// entered and offers the options to display a result label.
  /// </summary>
  [TemplatePart( Name = "PART_Text", Type = typeof( TextBox ) )]
  [TemplatePart( Name = "PART_Status", Type = typeof( TextBlock ) )]
  [TemplatePart( Name = "PART_EmptyText", Type = typeof( TextBlock ) )]
  public class SearchBox : Control
  {
    /// <summary>
    /// Static constructor.
    /// </summary>
    static SearchBox()
    {
      DefaultStyleKeyProperty.OverrideMetadata( typeof( SearchBox ), new FrameworkPropertyMetadata( typeof( SearchBox ) ) );
    }
 
    /// <summary>
    /// Text property.
    /// </summary>
    public String Text
    {
        get { return (String)GetValue(TextProperty); }
        set 
        { 
          SetValue(TextProperty, value);
        }
    }
 
    /// <summary>
    /// Status text. (i.e. 3 result(s))
    /// </summary>
    public String StatusText
    {
      get { return (String)GetValue( StatusTextProperty ); }
      set { SetValue( StatusTextProperty, value ); }
    }
 
    /// <summary>
    /// Empty text label.
    /// </summary>
    public String EmptyText
    {
      get { return (String)GetValue( EmptyTextProperty ); }
      set { SetValue( EmptyTextProperty, value ); }
    }
 
    /// <summary>
    /// Trigger to be notified of text changes.
    /// </summary>
    public event RoutedEventHandler TextChanged
    {
      add { AddHandler( TextChangedEvent, value ); }
      remove { RemoveHandler( TextChangedEvent, value ); }
    }
 
    /// <summary>
    /// Text property.
    /// </summary>
    public static readonly DependencyProperty TextProperty =
        DependencyProperty.Register( "Text", typeof( String ), typeof( SearchBox ), 
        new UIPropertyMetadata( null, new PropertyChangedCallback( OnTextChanged ),
                                      new CoerceValueCallback( OnCoerceText ) ) );
 
    /// <summary>
    /// Status text property.
    /// </summary>
    public static readonly DependencyProperty StatusTextProperty =
        DependencyProperty.Register( "StatusText", typeof( String ), typeof( SearchBox ), new UIPropertyMetadata( null ) );
 
    /// <summary>
    /// Empty text property.
    /// </summary>
    public static readonly DependencyProperty EmptyTextProperty =
        DependencyProperty.Register( "EmptyText", typeof( String ), typeof( SearchBox ), new UIPropertyMetadata( "search..." ) );
 
    /// <summary>
    /// Text change event property.
    /// </summary>
    public static readonly RoutedEvent TextChangedEvent = EventManager.RegisterRoutedEvent( "TextChanged",
      RoutingStrategy.Bubble, typeof( RoutedEventHandler ), typeof( SearchBox ) );
 
    /// <summary>
    /// Coerce callback when the text value changes.
    /// </summary>
    private static object OnCoerceText(DependencyObject o, Object value)
    {
      SearchBox searchBox = o as SearchBox;
      if ( searchBox != null )
        return searchBox.OnCoerceText( (String)value );
      else
        return value;
    }
 
    /// <summary>
    /// Called when the text property changes.
    /// </summary>
    private static void OnTextChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
    {
      SearchBox searchBox = o as SearchBox;
      if ( searchBox != null )
        searchBox.OnTextChanged( (String)e.OldValue, (String)e.NewValue );
    }
 
    /// <summary>
    /// Called to coerces value.
    /// </summary>
    protected virtual String OnCoerceText(String value)
    {
      return value;
    }
 
    /// <summary>
    /// Called when the text property changes.
    /// </summary>
    protected virtual void OnTextChanged(String oldValue, String newValue)
    {
      // Fire text changed event
      this.RaiseEvent( new RoutedEventArgs( SearchBox.TextChangedEvent, this ) );
 
      // Update the status text visibility based on the user text.
      UpdateStatusTextVisibility(newValue);
    }
 
    /// <summary>
    /// Handles key down events. Clears current text if escape is pressed.
    /// </summary>
    protected override void OnKeyDown(KeyEventArgs e)
    {
      // Clear the text search if escape is pressed.
      if ( e.Key == Key.Escape )
      {
        this.Text = "";   
      }
      else
      {
        base.OnKeyDown( e );
      }
    }
 
    /// <summary>
    /// Shows or hide the result label if user's text is displayed under it.
    /// </summary>
    /// <param name="newValue"></param>
    private void UpdateStatusTextVisibility(string newValue)
    {
      // Hide status if in the way
      TextBox textBox = GetTemplateChild( "PART_Text" ) as TextBox;
      TextBlock statusText = GetTemplateChild( "PART_Status" ) as TextBlock;
      if ( textBox != null && statusText != null )
      {
        // Compute the text width.
        FormattedText output = new FormattedText(newValue, CultureInfo.CurrentCulture, textBox.FlowDirection,
          new Typeface( textBox.FontFamily, textBox.FontStyle, textBox.FontWeight, textBox.FontStretch ), 
          textBox.FontSize, textBox.Foreground );
 
        // Get where the status text is displayed.
        Point relativePoint = statusText.TransformToAncestor( (Visual)statusText.Parent ).Transform( new Point( 0, 0 ) );
 
        // Set a custom bias.
        relativePoint.X -= textBox.FontSize;
 
        if ( output.Width >= relativePoint.X )
        {
          statusText.Visibility = Visibility.Hidden;
        }
        else
        {
          statusText.Visibility = Visibility.Visible;
        }
      }
    }
  }
}

Generic.xaml (XAML)

Download

<!--
/************************************************************************/
/*                  Engine42 - Editor Project (c) 2010                  */
/************************************************************************/
-->
 
<ResourceDictionary
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:controls="clr-namespace:Engine42.Editor.UI.Controls" 
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
  xmlns:local="clr-namespace:Engine42.Editor"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
 
  mc:Ignorable="d">
 
  <!-- SearchBox Control Style -->
 
  <Style TargetType="{x:Type controls:SearchBox}">
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="{x:Type controls:SearchBox}">
          <Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" 
                  BorderThickness="{TemplateBinding BorderThickness}">
            <Grid>
              <Grid.ColumnDefinitions>
                <ColumnDefinition />
                <ColumnDefinition Width="Auto" />
              </Grid.ColumnDefinitions>
              <TextBox x:Name="PART_Text" Grid.Column="0" 
                       Text="{Binding Path=Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, RelativeSource={RelativeSource TemplatedParent}}">
                <TextBox.Background>
                  <VisualBrush Stretch="None" AlignmentX="Left">
                    <VisualBrush.Transform>
                      <TranslateTransform X="5" Y="0"/>
                    </VisualBrush.Transform>
                    <VisualBrush.Visual>
                      <TextBlock x:Name="PART_EmptyText" Grid.Column="0" Text="{TemplateBinding EmptyText}" 
                                 Foreground="LightGray" FontSize="9" FontStyle="Italic" Opacity="0" Focusable="False"/>
                    </VisualBrush.Visual>
                  </VisualBrush>
                </TextBox.Background>
              </TextBox>
 
              <TextBlock x:Name="PART_Status" Grid.Column="0" Margin="0,-1,5,0" HorizontalAlignment="Right" VerticalAlignment="Center"
                         Text="{TemplateBinding StatusText}" Focusable="False" Foreground="LightSkyBlue" FontSize="9" 
                         FontStyle="Italic"/>
            </Grid>
          </Border>
 
          <ControlTemplate.Triggers>
            <MultiTrigger>
              <MultiTrigger.Conditions>
                <Condition SourceName="PART_Text" Property="Text" Value="" />
                <Condition SourceName="PART_Text" Property="IsFocused" Value="False" />
              </MultiTrigger.Conditions>
              <Setter Property="Opacity" Value="0.5" TargetName="PART_EmptyText"/>
            </MultiTrigger>
          </ControlTemplate.Triggers>
 
        </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>
 
</ResourceDictionary>
This entry was posted in Coding, WPF and tagged , , . Bookmark the permalink.

2 Responses to WPF Search Box custom control

  1. Barry says:

    Where’s the markup for the actual search text-box ?

  2. Jonathan says:

    It’s there under the header Generic.xaml.

Leave a Reply

Your email address will not be published. Required fields are marked *