Главная » Разработка для Windows Phone 7 » Обновления привязок TextBox

0

Свойство Text объекта TextBox может быть целью привязки данных, но при этом существуют потенциальные проблемы. Как только пользователь получает возможность вводить что-либо в TextBox, мы должны быть готовы к обработке ошибочного ввода.

Предположим, мы хотим написать приложение для решения квадратных уравнений, т.е. уравнений вида

Ах2 + Вх + С = О

Чтобы сделать приложение более универсальным, вероятно, необходимо предусмотреть три элемента управления TextBox, в которые пользователь сможет вводить значения A, B и C. Также включим кнопку Button с надписью «calculate» (Вычислить), по нажатию которой производится вычисление двух решений уравнения по стандартной формуле:

После этого решения выводятся в TextBlock.

Но багаж знаний о привязках данных и пример сервера привязки Adder наводят на мысли о применении другого подхода. Мы можем оставить три элемента управления TextBox и использовать TextBlock для отображения результатов, но при этом связать все эти элементы управления со свойствами сервера привязки.

А куда делся Button? Похоже, в Button нет особой необходимости.

Для начала возьмем из библиотеки Petzold.Phone.Silverlight класс под именем QuadraticEquationSolver (Модуль для решения квадратных уравнений). Он реализует интерфейс INotifyPropertyChanged, включает три свойства, названные A, B и C, а также свойства только для чтения Solution1 (Решение1) и Solution2. Есть еще два дополнительных свойства только для чтения типа bool: HasTwoSolutions (Имеет два решения) и HasOneSolution (Имеет одно решение).

Solution: Petzold.Phone.Silverlight Файл: QuadaticEquationSolver.cs

using System;

using System.ComponentModel;

namespace Petzold.Phone.Silverlight {

public class QuadraticEquationSolver : INotifyPropertyChanged {

Complex solution1; Complex solution2; bool hasTwoSolutions; double a, b, c;

public event PropertyChangedEventHandler PropertyChanged;

public double A {

set {

if (a != value) {

a = value;

0nPropertyChanged(new PropertyChangedEventArgs("A")); CalculateNewSolutions();

}

}

get {

return a;

}

}

public double B {

set {

if (b != value) {

b = value;

0nPropertyChanged(new PropertyChangedEventArgs("B")); CalculateNewSolutions();

}

}

get {

return b;

}

}

public double C {

set {

if (c != value) {

c = value;

0nPropertyChanged(new PropertyChangedEventArgs("C")); CalculateNewSolutions();

}

}

get {

return c;

}

}

public Complex Solution!

protected set {

if (!solution1.Equals(value)) {

solution1 = value;

OnPropertyChanged(new PropertyChangedEventArgs("Solution1"));

}

}

get {

return solution1;

}

}

public Complex Solution2 {

protected set {

if (!solution2.Equals(value)) {

solution2 = value;

OnPropertyChanged(new PropertyChangedEventArgs("Solution2"));

}

}

get {

return solution2;

}

}

public bool HasTwoSolutions {

protected set {

if (hasTwoSolutions != value) {

hasTwoSolutions = value; OnPropertyChanged(new PropertyChangedEventArgs("HasTwoSolutions"));

OnPropertyChanged(new

PropertyChangedEventArgs("HasOneSolution")); }

}

get {

return hasTwoSolutions;

}

}

public bool HasOneSolution {

get {

return !hasTwoSolutions;

}

}

void CalculateNewSolutions() {

if (A == 0 && B == 0 && C == 0) {

Solution1 = new Complex(0, 0); HasTwoSolutions = false; return;

if (A == 0) {

Solution1 = new Complex(-C / B, 0);

HasTwoSolutions = false;

return;

}

double discriminant = B * B – 4 * A * C; double denominator = 2 * A; double real = -B / denominator; double imaginary =

Math.Sqrt(Math.Abs(discriminant)) / denominator;

if (discriminant == 0) {

Solution1 = new Complex(real, 0); HasTwoSolutions = false; return;

}

Solution1 = new Complex(real, imaginary); Solution2 = new Complex(real, -imaginary); HasTwoSolutions = true;

}

protected virtual void 0nPropertyChanged(PropertyChangedEventArgs args) {

if (PropertyChanged != null)

PropertyChanged(this, args);

}

}

}

Свойства Solution1 и Solution2 типа Complex (Комплексный). Эта структура также включена в проект Petzold.Phone.Silverlight, но не реализует ни одной операции. Структура Complex существует исключительно для обеспечения методов ToString. (Silverlight 4 включает класс Complex в пространстве имен System.Numerics, но он недоступен в Silverlight for Windows Phone 7.)

Проект Silverlight: Petzold.Phone.Silverlight Файл: Complex.cs

using System;

namespace Petzold.Phone.Silverlight {

public struct Complex : IFormattable {

public double Real { get; set; } public double Imaginary { get; set; }

public Complex(double real, double imaginary) : this() {

Real = real; Imaginary = imaginary;

}

public override string ToString() {

if (Imaginary == 0)

return Real.ToString();

return String.Format("{0} {1} {2}i", Real,

Math.Sign(Imaginary) >= 1 ? "+" : "-",

Math.Abs(Imaginary));

public string ToString(string format, IFormatProvider provider) {

if (Imaginary == 0)

return Real.ToString(format, provider);

return String.Format(provider,

"{0} {1} {2}i",

Real.ToString(format, provider), Math.Sign(Imaginary) >= 1 ? "+" : "-", Math.Abs(Imaginary).ToString(format, provider));

}

}

}

Complex реализует интерфейс IFormattable (Поддающийся форматированию), т.е. у нее есть дополнительный метод ToString, включающий строку форматирования. Это необходимо, если мы собираемся использовать в методе String.Format спецификации форматирования числовых значений для форматирования комплексных чисел, как это делает StringFormatConverter.

Проект QuadraticEquationsl (Квадратные уравнения 1) – это первая попытка предоставления пользовательского интерфейса для класса Complex. Коллекция Resources класса MainPage включает ссылки на класс QuadraticEquationSolver и два конвертера, которые мы рассмотрели ранее:

Проект Silverlight: QuadraticEquations1 Файл: MainPage.xaml (фрагмент)

<phone:PhoneApplicationPage.Resources>

<petzold:QuadraticEquationSolver x:Key="solver" /> <petzold:StringFormatConverter x:Key="stringFormat" /> <petzold:BooleanToVisibilityConverter x:Key="booleanToVisibility" /> </phone:PhoneApplicationPage.Resources>

Область содержимого включает два вложенных элемента StackPanel. В горизонтальном StackPanel располагаются три элемента управления TextBox фиксированной ширины. Для них заданы двунаправленные привязки для ввода значений A, B и C. Обратите внимание, что свойству InputScope задано значение Number. Это обеспечит использование только цифровой клавиатуры.

Проект Silverlight: QuadraticEquations1 Файл: MainPage.xaml (фрагмент)

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">

<StackPanel DataContext="{Binding Source={StaticResource solver}}"> <StackPanel Orientation="Horizontal"

HorizontalAlignment="Center" Margin="12">

<TextBox Text="{Binding A, Mode=TwoWay}" InputScope="Number" Width="10 0" />

<TextBlock Text=" x" VerticalAlignment="Center" /> <TextBlock Text="2" VerticalAlignment="Center"> <TextBlock.RenderTransform>

<ScaleTransform ScaleX="0.7" ScaleY="0.7" /> </TextBlock.RenderTransform> </TextBlock>

<TextBlock Text=" + " VerticalAlignment="Center" />

<TextBox Text="{Binding B, Mode=TwoWay}" InputScope="Number" Width="100" />

<TextBlock Text=" x + " VerticalAlignment="Center" />

<TextBox Text="{Binding C, Mode=TwoWay}" InputScope="Number" Width="100" />

<TextBlock Text=" = 0" VerticalAlignment="Center" /> </StackPanel>

<TextBlock Text="{Binding Solution1,

Converter={StaticResource stringFormat}, ConverterParameter=’x = {0:F3}’}" HorizontalAlignment="Center" />

<TextBlock Text="{Binding Solution2,

Converter={StaticResource stringFormat}, ConverterParameter=’x = {0:F3}’}" Visibility="{Binding HasTwoSolutions,

Converter={StaticResource

booleanToVisibility}}"

HorizontalAlignment="Center" />

</StackPanel> </Grid>

Два элемента TextBlock, которые описываются в конце этого фрагмента, выводят на экран два решения. Свойство Visibility второго TextBlock связано со свойством HasTwoSolutions класса QuadraticEquationSolver, поэтому второй TextBlock не будет видимым, если уравнение имеет только одно решение.

Наверное, первое, на что сразу обращаешь внимание – это то, что введение числа в TextBox не влияет на решения! Сначала кажется, что приложение вообще не работает. Значение передается в свойство A, B или C класса QuadraticEquationSolver только после того, как TextBox теряет фокус ввода.

Такое поведение реализовано намеренно. В общем случае элементы управления могут быть связаны с бизнес-объектами по сети и, вероятно, не было бы ничего хорошего, если бы TextBox обновлялся при каждом касании клавиши экранной клавиатуры. Пользователи делают множество ошибок и часто удаляют введенные символы. В некоторых случаях того времени, которое пользователь тратит на ввод, абсолютно достаточно, чтобы «передать» окончательное значение.

Но в данном конкретном приложении такое поведение не соответствует нашим требованиям. Чтобы изменить его, зададим свойству UpdateSourceTrigger (Триггер обновления источника) объекта Binding каждого из элементов управления TextBox значение Explicit (Явный):

<TextBox Text="{Binding A, Mode=TwoWay,

UpdateSourceTrigger=Explicit}" InputScope="Number" Width="10 0" />

Свойство UpdateSourceTrigger управляет тем обновлением источника (в данном случае это свойство A, B или C класса QuadraticEquationSolver) из цели (TextBox) при двунаправленной привязке данных. Значением этого свойства является член перечисления UpdateSourceTrigger. В WPF-версии UpdateSourceTrigger также доступны члены LostFocus (Потеря фокуса) и PropertyChanged, но в Silverlight есть только два варианта: Default и Explicit.

Default означает «поведение по умолчанию для целевого элемента управления», что в случае, когда целью является TextBox, значит обновление объекта-источника при потере фокуса объектом TextBox. Если задано значение Explicit, должен быть предоставлен некоторый код, который будет инициировать передачу данных от цели в источник. Эту роль может выполнять Button с надписью «calculate».

Если не хотите использовать Button, можно инициировать передачу при изменении текста в TextBox. В этом случае кроме задания свойства UpdateSourceTrigger для Binding, потребуется обеспечить обработчик события TextChanged объекта TextBox:

<TextBox Text="{Binding A, Mode=TwoWay,

UpdateSourceTrigger=Explicit}" InputScope="Number" Width="10 0"

TextChanged="OnTextBoxTextChanged" />

В обработчике событий TextChanged необходимо «вручную» обновить источник, вызвав метод UpdateSource (Обновить источник), описанный классом BindingExpression (Выражение привязки).

Ранее в данной главе был продемонстрирован вызов метода SetBinding, описанного классом FrameworkElement, или статического метода BindingOperations.SetBinding для задания привязки для свойства в коде. (Метод SetBinding, описанный FrameworkElement, является сокращенным вариантом метода BindingOperations.SetBinding.) Оба метода возвращают объект типа BindingExpression.

Если вы не вызвали эти методы в коде, вам будет приятно узнать, что FrameworkElement сохраняет объект BindingExpression, и его можно извлечь с помощью открытого метода GetBindingExpression (Получить выражение привязки). В этот метод необходимо передать конкретное свойство, являющееся целью привязки данных, которое всегда, конечно же, будет свойством-зависимостью.

Рассмотрим код для обновления источника при изменении текста TextBox:

void OnTextBoxTextChanged(object sender, TextChangedEventArgs args) {

TextBox txtbox = sender as TextBox; BindingExpression bindingExpression = txtbox.GetBindingExpression(TextBox.TextProperty); bindingExpression.UpdateSource();

}

Еще одна проблема с TextBox – пользователь может ввести строку символов, которая не может быть распознана как число. Хотя мы и не замечаем этого, но для задания свойств A, B или C класса QuadraticEquationSolver объект string из TextBox преобразовывается в double с помощью конвертера. Этот скрытый конвертер, вероятно, использует метод Double.Parse или Double. TryParse.

Можно перехватывать исключения, формируемые конвертером. Для этого понадобится задать значение true еще двум свойствам класса Binding, как показано в следующем фрагменте:

<TextBox Text="{Binding A, Mode=TwoWay,

UpdateSourceTrigger=Explicit, Validates0nExceptions=True, Notify0nValidationError=True}" InputScope="Number" Width="100"

TextChanged="0nTextBoxTextChanged" />

Это приводит к формированию события BindingValidationError (Ошибка валидации привязки). Это маршрутизируемое событие, поэтому оно может обрабатываться в любом элементе дерева визуальных элементов, располагающемся над TextBox. Удобнее всего в небольшом приложении задавать обработчик события прямо в конструкторе MainPage:

readonly Brush okBrush;

static readonly Brush errorBrush = new SolidColorBrush(Colors.Red);

public MainPage() {

InitializeComponent();

okBrush = new TextBox().Foreground;

BindingValidationError += 0nBindingValidationError;

}

Заметьте, что обычная кисть Foreground для TextBox сохраняется как поле. Привожу простой обработчик события, который в случае недействительного ввода закрашивает текст TextBox красным:

void 0nBindingValidationError(object sender, ValidationErrorEventArgs args) {

TextBox txtbox = args.0riginalSource as TextBox; txtbox.Foreground = errorBrush;

}

Конечно, цвет текста должен возвращаться к исходному сразу же при изменении текста TextBox. Это можно сделать в методе OnTextBoxTextChanged (При изменении текста текстового поля):

void 0nTextBoxTextChanged(object sender, TextChangedEventArgs args) {

TextBox txtbox = sender as TextBox; txtbox.Foreground = okBrush;

}

Объединим эти две техники – обновления при каждом нажатии клавиши и визуализации недействительного ввода – в проекте QuadraticEquations2. Рассмотрим XAML-файл:

Проект Silverlight: QuadraticEquations2 Файл: MainPage.xaml (фрагмент) <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">

<StackPanel DataContext="{Binding Source={StaticResource solver}}"> <StackPanel 0rientation="Horizontal"

HorizontalAlignment="Center" Margin="12">

<TextBox Text="{Binding A, Mode=TwoWay,

UpdateSourceTrigger=Explicit, Validates0nExceptions=True, Notify0nValidationError=True}" InputScope="Number" Width="100"

TextChanged="0nTextBoxTextChanged" />

<TextBlock Text=" x" VerticalAlignment="Center" /> <TextBlock Text="2" VerticalAlignment="Center"> <TextBlock.RenderTransform>

<ScaleTransform ScaleX="0.7" ScaleY="0.7" /> </TextBlock.RenderTransform> </TextBlock>

<TextBlock Text=" + " VerticalAlignment="Center" />

<TextBox Text="{Binding B, Mode=TwoWay,

UpdateSourceTrigger=Explicit, Validates0nExceptions=True, Notify0nValidationError=True}" InputScope="Number" Width="100"

TextChanged="0nTextBoxTextChanged" />

<TextBlock Text=" x + " VerticalAlignment="Center" />

<TextBox Text="{Binding C, Mode=TwoWay,

UpdateSourceTrigger=Explicit, Validates0nExceptions=True, Notify0nValidationError=True}" InputScope="Number" Width="100"

TextChanged="0nTextBoxTextChanged" />

<TextBlock Text=" = 0" VerticalAlignment="Center" /> </StackPanel> <StackPanel Name="result"

0rientation="Horizontal" HorizontalAlignment="Center">

<TextBlock Text="{Binding Solution1.Real,

Converter={StaticResource stringFormat}, ConverterParameter=’x = {0:F3} ‘}" />

<TextBlock Text="+"

Visibility="{Binding Has0neSolution,

Converter={StaticResource

booleanToVisibility}}" />

<TextBlock Text="&#x00B1;"

Visibility="{Binding HasTwoSolutions,

Converter={StaticResource

booleanToVisibility}}" />

<TextBlock Text="{Binding Solution1.Imaginary,

Converter={StaticResource stringFormat}, ConverterParameter=’ {0:F3}i’}" />

</StackPanel> </StackPanel> </Grid>

Как видите, я полностью изменил представление решений. Вместо двух элементов TextBlock для отображения двух решений я использовал четыре элемента TextBlock и с их помощью вывожу на экран одно решение, которое может содержать знак ± (Unicode-код 0x00B1).

В файле выделенного кода реализованы обновление и обработка ошибок: Проект Silverlight: QuadraticEquationSolver2 Файл: MainPage.xaml.cs

using System;

using System.Windows;

using System.Windows.Controls;

using System.Windows.Data;

using System.Windows.Media;

using Microsoft.Phone.Controls;

namespace QuadraticEquationSolver2 {

public partial class MainPage : PhoneApplicationPage {

readonly Brush okBrush;

static readonly Brush errorBrush = new SolidColorBrush(Colors.Red);

public MainPage() {

InitializeComponent();

okBrush = new TextBox().Foreground;

BindingValidationError += OnBindingValidationError;

}

void OnTextBoxTextChanged(object sender, TextChangedEventArgs args) {

TextBox txtbox = sender as TextBox; txtbox.Foreground = okBrush;

BindingExpression bindingExpression =

txtbox.GetBindingExpression(TextBox.TextProperty); bindingExpression.UpdateSource();

}

void OnBindingValidationError(object sender, ValidationErrorEventArgs args) {

TextBox txtbox = args.OriginalSource as TextBox; txtbox.Foreground = errorBrush;

}

}

На снимке экрана представлен TextBox, указывающий на недействительность ввода:

Если вы уже писали приложение для решения квадратных уравнений для Windows Phone 7 до прочтения этой главы, его представление на экране может быть практически таким же, а вот структура, я думаю, совершенно иная. Я абсолютно уверен в этом, если приложение предназначалось для среды разработки с применением только кода, такой как Windows Forms.

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

Источник: Чарльз Петзольд, Программируем Windows Phone 7, Microsoft Press, © 2011.

По теме:

  • Комментарии