пятница, 21 августа 2009 г.

Editable Text с использование IValueConverter

Сегодня я хотел бы рассказать об использование IValueConverter интерфейсе и как используют ValueConvertor’ы в WPF.

ValueConvertor’ы очень полезная и удобная штука, которая очень часто используется при биндинге (будем по народному)для преобразования одного типа данных в другой, например:
  • Вы делаете компонент который отображает прогресс какого-то действия и вам надо менять цвет от зеленого до красного, в зависимости от того, на сколько прогресс движется к концу;
  • Вам надо сделать какой-то расчет, например зарплату по количеству часов * рейт работника;
  • Или более простые пример, это отформатировать дату или преобразовать цвет по #AABBCCDD в строковое представление;
  • Инвертировать отображения элемента, например у вас биндится свойство на CheckBox типо IsDeclined и если оно true, то CheckBox не должен быть отмечен (пример не надуманый, всякое бывает). Если есть возможность, то можно переименовать в Allowed и будет логично, а если нет? Тогда делаем сами уже знаете что.

Конверторы инкапсулируют какую-то логику, которая может меняться, они также могут принимать параметры. Также конверторы используются для отладки биндинга (об этом позже).

Я думаю у многих возникала потребность в редактируемом тексте, например отображается текст в метке “Edit me”, вы щелкаете по тексту, он меняется на TextBox, вы его редактируете, нажимаете Enter и опять метка, круто.

Рассмотрим как это можно сделать с использованием конверторов. Определим правила игры:

  1. Метка может иметь начальный текст
  2. Метка переходит в состояние редактирования по щелчку мышки на ней
  3. TextBox переходит в состояние метки после нажатия клавиши Enter или когда пользователь уводит курсор с TextBox или теряется фокус.

Рассмотрим XAML нашего компонента:

<UserControl x:Class="EditableLabel.Control.EditableText"
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
     xmlns:Converters="clr-namespace:EditableLabel.Converters"    
     x:Name="editableText">
     <UserControl.Resources>
          <Converters:BooleanToVisibleConverter x:Key="converter" />
     </UserControl.Resources>    
     <Grid>
          <Label x:Name="label" 
               Content="{Binding ElementName=editableTextBox, Path=Text}" 
               MouseLeftButtonDown="OnMouseLeftButtonClick"
               Visibility="{Binding AllowEdit, ElementName=editableText,
               Converter={StaticResource converter}, ConverterParameter=True}"/>

          <TextBox x:Name="editableTextBox" 
               LostFocus="OnTextBoxLostFocus" 
               MouseLeave="OnTextBoxMouseLeave" 
               KeyDown="OnTextBoxKeyDown" 
               Text="{Binding Text, ElementName=editableText}"
               Visibility="{Binding AllowEdit, ElementName=editableText,
               Converter={StaticResource converter}, ConverterParameter=False}"/>
     </Grid>
</UserControl>

У нас есть Label и TextBox. Когда AllowEdit свойство True отображается TextBox, в другом случае Label.

Текст для TextBox берется из свойства Text компонента, а текст для метки берется из TextBox. Зависимость выглядит так:

    Label.Text < –  TextBox.Text < – > UserControl.Text

В ресурсах у нас объявлен конвертер, который используется для свойства Visibility у Label и TextBox.

Visibility="{Binding AllowEdit, ElementName=editableText,
Converter={StaticResource converter}, ConverterParameter=False}
вот его код:
namespace EditableLabel.Converters
{
     [ValueConversion(typeof(bool), typeof(Visibility))]
     public class BooleanToVisibleConverter : IValueConverter
     {
          public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
          {
               bool flag = (bool) value;
               bool inverted = false;

               if (parameter != null)
               {
                    inverted = Boolean.Parse(parameter.ToString());
               }

               return inverted ? 
                    (flag ? Visibility.Hidden : Visibility.Visible) 
                    : (flag ? Visibility.Visible : Visibility.Hidden);
          }

          public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
          {
               throw new NotImplementedException();
          }
     }
}

Метод ConvertBack очень редко используется, в некоторых случаях при двунаправленном биндинге (расскажу в другой раз), поэтому всегда остается таким и не вызывается в данном случае.

В метод Convert, в параметр value приходит значение, которое мы биндим, в данном случае AllowEdit, а в parameter будет передано значение, которое мы указали в ConverterParameter (если не указали - null соответственно).

ConverterParameter не единственный способ передать параметры в класс конвертора. Если вам надо передать несколько параметров это можно сделать так:

[ValueConversion(typeof(bool), typeof(Visibility))]
public class BooleanToVisibleConverter : IValueConverter
{
     public string AdditionalValue { get; set; }

     public int AnotherValue { get; set; }

     public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
}

и затем использовать в разметке или в коде так:

<Converters:BooleanToVisibleConverter 
     x:Key="converter" 
     AdditionalValue="asd" 
     AnotherValue="2" />

Единственная проблема, если у вас биндятся ListBoxItem’s, например, и у каждого свое значение для конвертора, то такой способ объявления конвертора со свойствами не прокатит. Этот способ хорошо когда конвертор используется 1 раз. В другом случае придется использовать IMultiValueConverter. Принцип тот же самый, только в Convert передается object[] values.

ValueConversion атрибут используется для того, чтоб быстрей было понять, из какого в какой тип происходит конвертация. Этот атрибут не обязателен, просто best practice + удобство чтения.

Код событий:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace EditableLabel.Control
{
    /// <summary>
    /// Interaction logic for EditableText.xaml
    /// </summary>
    public partial class EditableText : UserControl
    {
        public static readonly DependencyProperty TextProperty =
            DependencyProperty.Register("Text", typeof (string), typeof (EditableText));

        public static readonly DependencyProperty AllowEditProperty =
            DependencyProperty.Register("AllowEdit", typeof (bool), typeof (EditableText));

        public bool AllowEdit
        {
            get { return (bool) GetValue(AllowEditProperty); }
            set { SetValue(AllowEditProperty, value); }
        }

        public string Text
        {
            get { return (string) GetValue(TextProperty); }
            set { SetValue(TextProperty, value); }
        }

        public EditableText()
        {
            InitializeComponent();
        }

        private void OnMouseLeftButtonClick(object sender, MouseButtonEventArgs e)
        {
            AllowEdit = true;
            editableTextBox.Focus();
        }

        private void OnTextBoxLostFocus(object sender, RoutedEventArgs e)
        {
            AllowEdit = false;
        }

        private void OnTextBoxMouseLeave(object sender, MouseEventArgs e)
        {
            AllowEdit = false;
        }

        private void OnTextBoxKeyDown(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Enter)
            {
                AllowEdit = false;
            }
        }
    }
}

Варианты для усовершенствования:
  • Cделать чтоб ширна TextBox’a была такой же как у метки;
  • Возможно вынести в отдельную сборку для дальнейшего использования и сделать как Custom Control, может потом захочется вид изменить или эффекты прикрутить.