пятница, 9 ноября 2012 г.

Подсказки в WPF компонентах - TextBox и ComboBox

   Первая публикация будет сразу вот такая техническая и посвященная решению проблемы отображения подсказок в WPF компонентах – TextBox и ComboBox.
   Итак, иногда бывает неплохо подсказать пользователю, какие текстовые данные необходимо ввести в TextBox, или обозначить, какой классификатор доступен в выпадающем списке. Конечно, можно щедро снабдить форму различными подписями рядом с полями для ввода, но, к сожалению, этого не всегда бывает достаточно.
   Поэтому ниже речь пойдет о том, как сделать вот такую красоту,


при этом не изменить обычного поведения TextBox и ComboBox.
   Начнем, пожалуй,  с простого  – реализуем поведение вспомогательной информационной подсказки для  TextBox. Требования к поведению – при получении фокуса ввода, либо в случае если свойству Text соответствует не пустое значение, подсказка должна пропадать. Очевидно, что свойство Text для отображения подсказки использовать нельзя, иначе компонент не позволит ввести пустую строку.  Помимо этого у TextBox нет стандартного поля для хранения строки содержащей подсказку.
   Пожалуй самый простой способ добавить поле в компонент – прибегнуть к присоединяемым свойствам. Класс, содержащий свойство для хранения подсказки будет иметь следующий вид:

public sealed class WaterMarkExtentions
    {
        public static string GetWaterMark(DependencyObject obj)
        {
            return (string)obj.GetValue(WaterMarkProperty);
        }

        public static void SetWaterMark(DependencyObject obj, string value)
        {
            obj.SetValue(WaterMarkProperty, value);
        }

        public static readonly DependencyProperty WaterMarkProperty =
           DependencyProperty.RegisterAttached("WaterMark"
                                       ,typeof(string)
                                       ,typeof(FrameworkElement)
                                       ,new FrameworkPropertyMetadata(""));
    }

   Далее целесообразно воспользоваться триггерами для того чтобы управлять отображением подсказки, соответственно необходимо описать стиль. А вот и он:

<Style TargetType="TextBox" x:Key="WaterMarkTextboxStyle">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="TextBox">
               <Border Background="{TemplateBinding Background}" 
                       BorderBrush="{TemplateBinding BorderBrush}"
                       BorderThickness="{TemplateBinding BorderThickness}" >
                   <Grid>
                       <ScrollViewer x:Name="PART_ContentHost" />
                       <TextBlock x:Name="WatermarkText"
                          Text="{Binding WaterMark, 
                              RelativeSource={RelativeSource TemplatedParent}}"
                          Foreground="Gray" Margin="5,0,0,0" 
                          HorizontalAlignment="Left" 
                          VerticalAlignment="Center" 
                          Visibility="Collapsed" 
                          IsHitTestVisible="False"/>
                   </Grid>
               </Border>
               <ControlTemplate.Triggers>
                   <MultiTrigger>
                      <MultiTrigger.Conditions>
                        <Condition Property="IsKeyboardFocusWithin" 
                                   Value="False"/>
                        <Condition Property="Text" Value=""/>
                      </MultiTrigger.Conditions>
                   <Setter Property="Visibility" 
                           TargetName="WatermarkText" 
                           Value="Visible"/>
                   </MultiTrigger>
                   <MultiTrigger>
                      <MultiTrigger.Conditions>
                        <Condition Property="IsKeyboardFocusWithin" 
                                   Value="False"/>
                        <Condition Property="Text" Value="{x:Null}"/>
                       </MultiTrigger.Conditions>
                   <Setter Property="Visibility"
                           TargetName="WatermarkText" 
                           Value="Visible"/>
                   </MultiTrigger>
               </ControlTemplate.Triggers>
           </ControlTemplate>
         </Setter.Value>
       </Setter>
    </Style>

   Подсказка отображается в TextBlock (WatermarkText), который располагается над контентом нашего TextBox. Свойством Visibility у  WatermarkText управляют триггеры в зависимости от того есть ли фокус ввода у компонента, и от значения свойства Text.
Собственно все, компонент готов к использованию. Ниже пример использования:

<Window x:Class="WaterMarkStyle.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:System="clr-namespace:System;assembly=mscorlib"
        xmlns:local="clr-namespace:WaterMarkStyle"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
     <TextBox 
        local:WaterMarkExtentions.WaterMark = "sample textbox watermark text"
        Style="{StaticResource WaterMarkTextboxStyle}"    
        HorizontalAlignment="Left"/>
    </Grid>
</Window>
 
   Далее, как и обещал в начале, тот же фокус выполним для ComboBox. Идея, в общем, та же, но вот шаблон компонента значительно сложнее, его можно найти на msdn. Теперь все что осталось – это переработать описание стиля шаблона для поля ввода, которое называется –  PART_EditableTextBox. Стиль описанный ранее (WaterMarkTextboxStyle) великолепно сюда подходит, однако, в виду сложности шаблона для ComboBox, придется поправить Binding подсказки следующим образом:

  <TextBlock x:Name="WatermarkText" 
Text="{Binding WaterMark, RelativeSource=
       {RelativeSource Mode=FindAncestor, AncestorType=ComboBox}}"
Foreground="Gray" Margin="5,0,0,0" 
HorizontalAlignment="Left" VerticalAlignment="Center"
Visibility="Collapsed" IsHitTestVisible="False"/>

   Ну и последний штрих – теперь за отображение подсказки будет отвечать еще и выбранный в выпадающем списке элемент. Помимо этого надо не забывать про флаг IsEditable. Поэтому добавим в триггеры шаблона ComboBox:

<ControlTemplate.Triggers>
...
<MultiTrigger>
      <MultiTrigger.Conditions>
           <Condition Property="IsEditable" Value="false"/>
           <Condition Property="SelectedItem" Value="{x:Null}"/>
      </MultiTrigger.Conditions>
   <Setter TargetName="PART_EditableTextBox" 
           Property="Visibility"   
           Value="Visible"/>
</MultiTrigger>

   Пример использования будет абсолютно аналогичный:

  <ComboBox 
        local:WaterMarkExtentions.WaterMark = "sample combobox watermark text"
         Style="{StaticResource WaterMarkComboBoxStyle}" >
     <System:String>test1</System:String>
     <System:String>test2</System:String>
 </ComboBox>