Преобразование данных

140

»» В ДАННОЙ СТАТЬЕ ИСПОЛЬЗУЕТСЯ ИСХОДНЫЙ КОД ДЛЯ ПРИМЕРОВ

При обычной привязке информация путешествует от источника к цели без каких-либо изменений. Это кажется логичным, но такое поведение не всегда подходит. Часто источник данных может использовать низкоуровневое представление, которое не нужно отображать непосредственно в пользовательском интерфейсе.

Например, может понадобиться, чтобы числовые коды заменялись читабельными для человека строками, числа представлялись в укороченном виде, даты отображались в длинном формате и т.д. Если это так, то нужен какой-то способ преобразования этих значений в корректную отображаемую форму. И если применяется двунаправленная привязка, то также понадобится обратная операция — преобразование введенных пользователем данных в представление, подходящее для хранения в соответствующем объекте данных.

К счастью, в WPF доступны два средства, которые могут оказать помощь:

Форматирование строк

Это средство позволяет преобразовать данные, представленные в виде текста — например, строки, которые содержат даты и числа, — за счет установки свойства Binding.StringFormat. Это удобный прием, который справляется, по крайней мере, с половиной задач форматирования.

Конвертеры значений

Это намного более мощное (и в некотором отношении более сложное) средство, позволяющее преобразовывать любой тип исходных данных в любой тип представления объекта, который может быть передан связанному элементу управления.

Оба подхода рассматриваются в последующих разделах.

Свойство StringFormat

Форматирование строк — блестящий инструмент для форматирования чисел, которые нужно отобразить в виде текста. Например, возьмем свойство Cost из класса CarTable. Cost хранится как double, и в результате его отображения в текстовом поле можно наблюдать значение вроде 150000. Такой формат пропускает символ валюты и не слишком удобен для чтения. Интуитивно понятное представление должно выглядеть как $150.000 или 150.000 руб.

Простейшее решение состоит в установке свойства Binding.StringFormat. Для преобразования неформатированного текста в его отображаемое значение непосредственно перед его появлением в элементе управления WPF использует форматную строку. Не менее важно, что WPF в большинстве случаев применяет эту строку для обратного преобразования, взяв любые отредактированные данные и используя их для обновления привязанного свойства.

При установке свойства Binding.StringFormat применяются стандартные форматные строки .NET вида {0:С}. Здесь 0 представляет первое значение, а С ссылается на строку формата, которой в данном случае является стандартный, специфичный для локали формат валюты, который преобразует 3.99 в $3.99 на компьютере, находящемся в США.

Все выражение помещается в фигурные скобки. Ниже приведен пример применения форматной строки к полю Cost, чтобы его значение отображалось как денежное (здесь и далее используется полный пример из статьи привязка данных и LINQ):

<TextBox Margin="5" Grid.Row="2" Grid.Column="1" 
            Text="{Binding Cost, ValidatesOnExceptions=True, StringFormat={}{0:C}}" ...

Значение StringFormat предварено еще одной парой фигурных скобок {}. Вместе получается {}{0:С}, а не просто {0:С}. Эта несколько неуклюжая конструкция необходима, чтобы защитить строку. В противном случае анализатор XAML может быть введен в заблуждение фигурной скобкой в начале {0:С}. Кстати, управляющая последовательность {} необходима, только когда значение StringFormat начинается со скобки.

Т.к. в базе данных стоимость хранится в рублях, а не в долларах давайте модифицируем выражение привязки:

<TextBox Margin="5" Grid.Row="2" Grid.Column="1" 
            Text="{Binding Cost, ValidatesOnExceptions=True, StringFormat={}{0} руб.}" ...
Пример использования форматирующих строк в WPF

Однако эта форматная строка работает только в одном направлении. Если пользователь попытается применить отредактированное значение, включающее литеральный текст, обновление потерпит неудачу. С другой стороны, если пользователь выполнит редактирование, введя только числовое значение, то редактирование удастся, и выражение привязки преобразует его в отображаемый текст. Ниже показан пример ошибки:

Ошибка ввода строки

Чтобы получить нужный результат с помощью свойства StringFormat, понадобится правильная строка формата. Все доступные форматные строки можно найти в справочной системе Visual Studio. Ниже перечислены наиболее часто используемые опции, которые будут применять для числовых данных и дат соответственно.

Строки формата для числовых данных
Тип Строка формата Пример
Валюта C $1,234.50
Отрицательные значения представляются в скобках: ($1,234.50). Знак валюты специфичен для локали.
Научный (экспоненциальный) E 1.234.50Е+004
Процентный P 45.6%
Десятичный с фиксированной точкой F? Зависит от количества установленных десятичных разрядов. F3 форматирует значения в виде 123.400, a F0 форматирует значения подобно 123
Строки формата для времени и дат
Тип Строка формата Пример
Короткая дата d M/d/yyyy. Например: 10/30/2010.
Длинная дата D dddd, MMMM dd, yyyy. Например: Monday, January 31, 2011.
Длинная дата и короткое время f dddd, MMMM dd, yyyy HH:mm aa. Например: Monday, January 31, 2011 10:00 AM.
Длинная дата и длинное время F dddd, MMMM dd, yyyy HH:mm:ss aa. Например: Monday, January 31, 2011 10:00:23 AM.
Сортируемый стандарт ISO s yyyy-MM-dd HH:mm:ss. Например: 2011-01-31 10:00:23.
Месяц и день M MMMM dd. Например: January 31.
Общий G M/d/yyyy HH:mm:ss aa (зависит от локальных установок). Например: 10/30/2010 10:00:23 AM.

Списочные элементы управления WPF также поддерживают строковое форматирование для своих элементов. Чтобы использовать его, нужно просто установить свойство ItemStringFormat списка (унаследованное от базового класса ItemsControl). Ниже приведен пример со списком машин:

<ListBox Name="lstCars" Margin="5" ItemStringFormat="Марка: {0}" />

Форматированная строка автоматически передается привязке, которая захватывает текст каждого элемента списка.

Конвертеры значений

Свойство Binding.StringFormat создано для простого стандартного форматирования чисел и дат. Но во многих сценариях привязки требуется более мощный инструмент, который называется классом конвертера значений.

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

Конвертеры значений — исключительно удобная часть инфраструктуры привязки данных WPF. Их можно использовать несколькими удобными способами, которые перечислены ниже:

Форматирование строк с помощью конвертера значений

Чтобы получить базовое представление о работе конвертера значений, вернемся к примеру форматирования денежной величины из предыдущего раздела. Хотя там использовалось свойство Binding. StringFormat, аналогичного (и даже большего) результата можно добиться с помощью конвертера значений. Например, можно округлять или усекать значения, использовать словесное описание числа (изменив 600000 на 600 тыс. руб.). Можно даже настроить работу обратного преобразования, заменяя введенные пользователем значения правильными значениями в привязанном объекте (чтобы избавиться от ошибки, возникающей при редактировании данных).

Для создания конвертера значений потребуется выполнить четыре шага:

  1. Создать класс, реализующий IValueConverter.

  2. Добавить атрибут ValueConversion в объявление класса и указать исходный и целевой типы данных.

  3. Реализовать метод Convert(), преобразующий данные из исходного формата в отображаемый формат.

  4. Реализовать метод ConvertBack(), выполняющий обратное преобразование значения из отображаемого формата в его "родной" формат.

Ниже приведен полный код конвертера значений, который имеет дело со значениями цены, хранимыми в свойстве CarTable.Cost:

[ValueConversion(typeof(double), typeof(string))]
public class CostConverter : IValueConverter
{
        public object Convert(object value, Type targetType, object parameter,
            System.Globalization.CultureInfo culture)
        {
            // Возвращаем строку в формате 123.456.789 руб.
            return ((double)value).ToString("#,###", culture) + " руб.";
        }

        public object ConvertBack(object value, Type targetType, object parameter,
            System.Globalization.CultureInfo culture)
        {
            double result;
            if (Double.TryParse(value.ToString(), System.Globalization.NumberStyles.Any,
                         culture, out result))
            {
                return result;
            }
            else if (Double.TryParse(value.ToString().Replace(" руб.", ""), System.Globalization.NumberStyles.Any,
                         culture, out result))
            {
                return result;
            }
            return value;
        }
}

Чтобы ввести в действие этот конвертер, надо начать с отображения пространства имен проекта на префикс пространства имен XML, который можно применять в коде разметки. Если вы используете старый пример, то такой префикс уже существует:

<Window ...
xmlns:databinding="clr-namespace:DataBinding"

Далее нужно добавить объект конвертора в ресурсы окна:

<Window.Resources>
        <databinding:CostConverter x:Key="CostConverter"></databinding:CostConverter>
</Window.Resources>

Затем можно указывать на него в привязке, используя ссылку StaticResource:

<TextBox Margin="5" Grid.Row="2" Grid.Column="1" 
           Text="{Binding Cost, ValidatesOnExceptions=True, Converter={StaticResource CostConverter}}"
           LostFocus="textChange_Event"></TextBox>

Теперь значения, введенные пользователем в указанном формате будут корректно преобразовываться и записываться в базу данных:

Корректная обработка данных с помощью конверторов значений

Создание объектов с конвертером значений

Конвертеры значений незаменимы, когда необходимо преодолеть зазор между способом сохранения данных в классах и способом их отображения в окне. Например, предположим, что есть графические данные, хранящиеся в виде массива байт в поле базы данных.

Существует возможность преобразования двоичных данных в объект System.Windows.Media.Imaging.BitmapImage и сохранения его как части объекта данных. Однако такое проектное решение может не подойти.

Например, может понадобиться гибкость для создания более одного объектного представления изображения — возможно, потому, что библиотека данных используется как в приложениях WPF, так и в приложениях Windows Forms (где вместо этого применяется класс System.Drawing.Bitmap). В таком случае имеет смысл хранить низкоуровневую двоичную информацию в объекте данных и преобразовывать ее в WPF-объект BitmapImage с помощью конвертера значений.

Таблица CarTable из базы данных AutoShop не включает двоичных графических данных, но содержит поле ImageCar, хранящее имя файла с изображением машины.

Во-первых, изображение может быть недоступным в зависимости от расхода памяти при работе приложения. Во-вторых, нет смысла в расходе дополнительной памяти для хранения изображения, если вы не собираетесь его показывать.

Поле ImageCar включает имя файла и относительный путь, но не полный путь к этому файлу изображения, что дает определенную гибкость для размещения таких файлов в любом подходящем месте. Перед конвертером значений стоит задача создания URI, указывающего на файл изображения, на основе поля CarImage и каталога, который будет использоваться для хранения таких файлов. Ниже приведен полный код ImagePathConverter, выполняющий преобразование:

public class ImagePathConverter : IValueConverter
{
        public object Convert(object value, Type targetType, object parameter,
            System.Globalization.CultureInfo culture)
        {
            return new BitmapImage(
                new Uri(
                    System.IO.Directory.GetCurrentDirectory() + "\\" + (string)value));
        }

        public object ConvertBack(object value, Type targetType, object parameter,
            System.Globalization.CultureInfo culture)
        {
            return null;
        }
}

Давайте немного изменим компоновку нашего проекта и добавим выражение привязки, использующее этот конвертер значений, в объект Image (не забудьте добавить этот конвертор в ресурсы окна, например, под именем ImageConverter):

<!-- Файл MainWindow.xaml -->

...

<Grid  Name="gridCarDetails" DataContext="{Binding ElementName=lstCars, Path=SelectedItem}">
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="110"/>
                                <ColumnDefinition Width="Auto"/>
                                <ColumnDefinition/>
                            </Grid.ColumnDefinitions>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="Auto"></RowDefinition>
                                <RowDefinition Height="Auto"></RowDefinition>
                                <RowDefinition Height="Auto"></RowDefinition>
                                <RowDefinition Height="Auto"></RowDefinition>
                                <RowDefinition Height="*"></RowDefinition>
                            </Grid.RowDefinitions>
                            
                            <Image Margin="5,0" Grid.RowSpan="3"
                                   Source="{Binding Path=ImageCar, Converter={StaticResource ImageConverter}}"/>
                            <TextBlock Margin="7" Grid.Column="1">Марка:</TextBlock>
                            <TextBox Margin="5" Grid.Column="2" LostFocus="textChange_Event">
                                <TextBox.Text>
                                    <Binding Path="ModelName" UpdateSourceTrigger="PropertyChanged">
                                        <Binding.ValidationRules>
                                            <databinding:EmptyRule></databinding:EmptyRule>
                                        </Binding.ValidationRules>
                                    </Binding>
                                </TextBox.Text>
                            </TextBox>
                            <TextBlock Margin="7" Grid.Row="1" Grid.Column="1">Модель:</TextBlock>
                            <TextBox Margin="5" Grid.Row="1" Grid.Column="2" LostFocus="textChange_Event">
                                <TextBox.Text>
                                    <Binding Path="ModelNumber" UpdateSourceTrigger="PropertyChanged">
                                        <Binding.ValidationRules>
                                            <databinding:EmptyRule></databinding:EmptyRule>
                                        </Binding.ValidationRules>
                                    </Binding>
                                </TextBox.Text>
                            </TextBox>
                            <TextBlock Margin="7" Grid.Row="2" Grid.Column="1">Цена (руб):</TextBlock>
                            <TextBox Margin="5" Grid.Row="2" Grid.Column="2" 
                                     Text="{Binding Cost, ValidatesOnExceptions=True, Converter={StaticResource CostConverter}}"
                                     LostFocus="textChange_Event"></TextBox>
                            <TextBlock Margin="7,7,7,0" Grid.Row="3">Описание:</TextBlock>
                            <TextBox Margin="7" Grid.Row="4" Grid.ColumnSpan="3"
                                     VerticalScrollBarVisibility="Visible" TextWrapping="Wrap" 
                                     Text="{Binding Path=Description, TargetNullValue=Описание не доступно}" LostFocus="textChange_Event"/>
</Grid>

Данный код (в частности привязка свойства Source объекта Image) работает, поскольку свойство Image.Source ожидает объекта ImageSource, a класс BitmapImage унаследован от ImageSource. Результат показан ниже:

Использование конвертора, возвращающего объект

Этот пример можно усовершенствовать несколькими способами. Попытка создать BitmapImage, указывающий на несуществующий файл, вызовет исключение, которое будет получено при установке свойств DataContext, ItemsSource или Source.

В качестве альтернативы можно добавить свойства в класс ImagePathConverter, которые позволят настроить это поведение. Например, можно предусмотреть свойство SuppressExceptions булевского типа. Если оно установлено в true, можно перехватывать исключения в методе Convert() и затем возвращать значение Binding.DoNothing (которое укажет WPF действовать так, как будто никакой привязки данных не установлено).

Или же можно добавить свойство DefaultImage, которое будет принимать BitmapImage. Тогда в случае возникновения исключений ImagePathConverter сможет вернуть изображение, выбранное по умолчанию.

Также следует отметить, что этот конвертер поддерживает только однонаправленное преобразование. Причина в том, что изменить объект BitmapImage невозможно и применять его для обновления пути к изображению. Однако можно воспользоваться альтернативным подходом. Вместо возврата BitmapImage из ImagePathConverter можно просто вернуть полностью квалифицированный URI из метода Convert().

Применение условного форматирования

Некоторые из наиболее интересных конвертеров значений не предназначены для форматирования данных для целей презентации. Вместо этого они служат для форматирования ряда других связанных с внешним видом аспектов элемента на основе правила данных.

Например, предположим, что требуется выделить самые дорогостоящие машины, окрасив фон другим цветом. Эту логику легко инкапсулировать в следующем конвертере значений:

public class CostToBackgroundConverter : IValueConverter
{
        public double MinimumCostRichCar { get; set; }
        public Brush HighlughtBrush { get; set; }
        public Brush DefaultBrush { get; set; }

        public object Convert(object value, Type targetType, object parameter,
            System.Globalization.CultureInfo culture)
        {
            if ((double)value >= MinimumCostRichCar)
                return HighlughtBrush;
            else
                return DefaultBrush;
        }

        public object ConvertBack(object value, Type targetType, object parameter,
            System.Globalization.CultureInfo culture)
        {
            return null;
        }
}

Этот конвертер значений тщательно спроектирован с учетом возможности повторного использования. Вместо жесткого кодирования, цвета для выделения задаются в XAML-размете кодом, который использует этот конвертер:

<Window.Resources>
        ...
        <databinding:CostToBackgroundConverter x:Key="bgConverter" MinimumCostRichCar="1000000"
                                               DefaultBrush="{x:Null}" HighlughtBrush="LightSalmon"/>
        <Style x:Key="myLBIStyle" TargetType="{x:Type ListBoxItem}">
            <Setter Property="Background" Value="{Binding Path=Cost, Converter={StaticResource bgConverter}}"/>
        </Style>
    </Window.Resources>

...

<!-- Добавили стиль для элементов ListBoxItem -->
<ListBox Name="lstCars" Margin="5" ItemContainerStyle="{StaticResource myLBIStyle}"/>

...

Вместо цветов применяются кисти, так что можно создавать и более совершенные эффекты выделения, применяя градиенты и фоновые изображения. Чтобы сохранить стандартный прозрачный фон (таким образом, будет использоваться фон родительского элемента), просто установите свойство DefaultBrush или HighlightBrush в Null, как показано выше. Теперь машины, стоящие выше 1 млн. руб. будут подсвечены в списке:

Условное форматирование с помощью конвертеров данных

Оценка множества свойств

До сих пор выражения привязки использовались для преобразования одной части исходных данных в единый форматированный результат. И хотя изменить вторую часть уравнения (результат) не удастся, приложив немного усилий, можно создать привязку, которая вычисляет или комбинирует информацию из более чем одного исходного свойства.

Первый трюк состоит в замене объекта Binding на MultiBinding. Затем в свойстве MultiBinding.StringFormat определяется организация привязанных свойств. Ниже приведен пример, который объединяет фамилию с именем и отображает результат в TextBlock:

<TextBlock> 
   <TextBlock.Text> 
       <MultiBinding StringFormat="{}{1}, {0}"> 
           <Binding Path="FirstName"></Binding> 
           <Binding Path="LastName"></Binding> 
       </MultiBinding> 
   </TextBlock.Text> 
</TextBlock> 

В этом примере два поля используются, как они есть, в свойстве StringFormat. В качестве альтернативы можно применять форматные строки, чтобы изменить это. Например, при комбинировании с помощью MultiBinding текстового значения и значения валюты можно установить StringFormat в "{0} затраты {1:C}".

Если хотите сделать с этими двумя исходными полями что-то более изощренное, чем просто соединить их вместе, понадобится помощь конвертера значений. Такой прием позволяет производить вычисления либо применять форматирование, которое принимает во внимание несколько деталей (таких как подсветка всех товаров с наивысшей ценой в указанной категории). Однако для этого конвертер значений должен реализовывать интерфейс IMultiValueConverter вместо IValueConverter.

Пройди тесты
Лучший чат для C# программистов