The Text
property of a TextBox can be a target of a data binding, but some potential problems
are introduced. Once you allow the user to type anything into a TextBox, you need to deal
with faulty input.
Suppose you want to write a
program that solves quadratic equations, that is, solutions of the
equation
To make the program most versatile, you’d probably
supply three TextBox
controls to allow the user to type in values of A, B, and C.
You could then include a Button labeled “calculate” that obtains the two solutions
from the standard equation:
You’d then display the
solutions in a TextBlock.
With what you know about data bindings (and considering the example of
the Adder
binding server), a somewhat different approach comes to mind. This
approach retains the three TextBox controls and uses a TextBlock
to display results.
These controls are all bound to properties of the binding server.
So where does the Button go? Well, perhaps the Button isn’t really needed.
To get started, here’s a
class from Petzold.Phone.Silverlight named QuadraticEquationSolver.
It implements the INotifyPropertyChanged
interface, has three properties named A, B,
and C, and get-only properties named Solution1 and Solution2. Two additional read-only properties are of type bool and named HasTwoSolutions
and HasOneSolution.
Example 1. Solution:
Petzold.Phone.Silverlight File: 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; OnPropertyChanged(new PropertyChangedEventArgs("A")); CalculateNewSolutions(); } } get { return a; } }
public double B { set { if (b != value) { b = value; OnPropertyChanged(new PropertyChangedEventArgs("B")); CalculateNewSolutions(); } } get { return b; } }
public double C { set { if (c != value) { c = value; OnPropertyChanged(new PropertyChangedEventArgs("C")); CalculateNewSolutions(); } } get { return c; } }
public Complex Solution1 { 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 OnPropertyChanged(PropertyChangedEventArgs args) { if (PropertyChanged != null) PropertyChanged(this, args); } } }
|
The Solution1 and Solution2 properties are of type Complex, a structure that is also included in
the Petzold.Phone.Silverlight project but which doesn’t implement any
operations. The structure exists solely to provide ToString methods. (Silverlight 4 includes a Complex class
in its System.Numerics namespace but this is not available in Silverlight for
Windows Phone 7.)
Example 2. Silverlight
Project: Petzold.Phone.Silverlight File: 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 implements
the IFormattable
interface, which means it has an additional ToString method that includes a formatting string.
This is necessary if you’re going to use numeric formatting
specifications in String.Format to
format these complex numbers, as StringFormatConverter
does.
The QuadraticEquations1
project is a first attempt at providing a user interface for this
class. The Resources collection of MainPage contains references to the QuadraticEquationSolver class and two converters that you’ve seen
before:
Example 3. Silverlight
Project: QuadraticEquations1 File: MainPage.xaml (excerpt)
<phone:PhoneApplicationPage.Resources> <petzold:QuadraticEquationSolver x:Key="solver" /> <petzold:StringFormatConverter x:Key="stringFormat" /> <petzold:BooleanToVisibilityConverter x:Key="booleanToVisibility" /> </phone:PhoneApplicationPage.Resources>
|
The content area has two
nested StackPanel elements. The
horizontal StackPanel contains three TextBox controls of fixed
width with two-way bindings for typing in values of A, B, and C.
Notice that the InputScope is set to Number for a specifically numeric keyboard.
Example 4. Silverlight
Project: QuadraticEquations1 File: MainPage.xaml (excerpt)
<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="100" />
<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>
|
The two TextBlock elements at the end display the two solutions; the
second TextBlock has its Visibility property bound to the HasTwoSolutions property of QuadraticEquationSolver
so it’s not visible if the equation has only one solution.
Probably the first thing
you’ll notice is that typing a number into a TextBox has no effect on the solutions! At first it
seems like the program is not working at all. Only when the TextBox you’ve been typing into loses input focus does the
value get transferred to the A, B, or C property of the QuadraticEquationSolver class.
This behavior is by design.
In the general case, controls could be bound to business objects over a
network, and you probably don’t want an object bound to a TextBox being
updated with every little keystroke. Users make a lot of mistakes and
perform a lot of backspacing and in some cases waiting until the user
has finished is really the proper time to “submit” the final value.
In this particular program,
that behavior is probably not what you want. To change it, you’ll want
to set the UpdateSourceTrigger property
of the Binding in each of the TextBox controls to Explicit:
<TextBox Text="{Binding A, Mode=TwoWay,
UpdateSourceTrigger=Explicit}"
InputScope="Number"
Width="100" />
The UpdateSourceTrigger
property governs how the source (in this
case, the A, B, or C property in QuadraticEquationSolver) is updated from the target (the TextBox) in a two-way
binding. The property is set to a member of the UpdateSourceTrigger enumeration. In the WPF
version of UpdateSourceTrigger, members
named LostFocus and PropertyChanged are
available, but in Silverlight the only two options are Default and Explicit.
Default means “the default
behavior for the target control” which for a TextBox
target means that the source
is updated when the TextBox loses
focus. When you specify Explicit, you need to provide some code that triggers
the transfer of data
from the target to the source. This could be the role of a Button labeled “calculate.”
If you’d rather avoid that Button, you can
trigger the transfer when the text changes in the TextBox, so in
addition to setting the UpdateSourceTrigger
property of Binding,
you need to provide a handler for the TextChanged
event of the TextBox:
<TextBox Text="{Binding A, Mode=TwoWay,
UpdateSourceTrigger=Explicit}"
InputScope="Number"
Width="100"
TextChanged="OnTextBoxTextChanged" />
In the TextChanged event handler, you need to “manually” update
the source by calling the UpdateSource
method defined by BindingExpression.
Earlier in this artilce, I
showed you how to call the SetBinding
method defined by FrameworkElement or
the static BindingOperations.SetBinding method to set a binding on a property in code.
(The SetBinding method defined by FrameworkElement is just a
shortcut for BindingOperations.SetBinding.)
Both methods return an object of type BindingExpression.
If you haven’t called these
methods in code, you’ll be pleased to learn that FrameworkElement
stores the BindingExpression object so it can be retrieved with the public GetBindingExpression
method. This method requires the particular property that is the target
of the data binding, which is always, of course, a dependency property.
Here’s the code for updating the source when the TextBox
text changes:
void OnTextBoxTextChanged(object sender, TextChangedEventArgs args)
{
TextBox txtbox = sender as TextBox;
BindingExpression bindingExpression = txtbox.GetBindingExpression(TextBox.TextProperty);
bindingExpression.UpdateSource();
}
Another problem with the TextBox is that the user can enter a character string
that cannot be resolved to a number. Although you can’t see it, a
converter is at work converting the string
object from the TextBox to a double to set to the A, B, or C
property of QuadraticEquationSolver.
This hidden converter probably uses the Double.Parse
or Double.TryParse method.
If you’d like to catch exceptions raised by this converter, you can. You’ll
probably want to set two more properties of the Binding class to true,
as shown here:
<TextBox Text="{Binding A, Mode=TwoWay,
UpdateSourceTrigger=Explicit,
ValidatesOnExceptions=True,
NotifyOnValidationError=True}"
InputScope="Number"
Width="100"
TextChanged="OnTextBoxTextChanged" />
This causes a BindingValidationError
event to be fired. This is a routed event, so it can be handled anywhere in
the visual tree above the TextBox. Most conveniently in a small program, a handler for
the event can be set right in the MainPage constructor:
readonly Brush okBrush;
static readonly Brush errorBrush = new SolidColorBrush(Colors.Red);
public MainPage()
{
InitializeComponent();
okBrush = new TextBox().Foreground;
BindingValidationError += OnBindingValidationError;
}
Notice that the normal Foreground
brush of the TextBox
is saved as a field. Here’s a simple event handler that colors the text
in the TextBox red if it’s invalid:
void OnBindingValidationError(object sender, ValidationErrorEventArgs args)
{
TextBox txtbox = args.OriginalSource as TextBox;
txtbox.Foreground = errorBrush;
}
Of course, as soon the TextBox text changes again, you’ll want to restore that
color, which you can do in the OnTextBoxTextChanged
method:
void OnTextBoxTextChanged(object sender, TextChangedEventArgs args)
{
TextBox txtbox = sender as TextBox;
txtbox.Foreground = okBrush;
. . .
}
These two techniques—updating
with each keystroke and giving a visual indication of invalid input—are combined in the QuadraticEquations2 project.
Here’s the XAML file:
Example 5. Silverlight
Project: QuadraticEquations2 File: MainPage.xaml (excerpt)
<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, UpdateSourceTrigger=Explicit, ValidatesOnExceptions=True, NotifyOnValidationError=True}" InputScope="Number" Width="100" TextChanged="OnTextBoxTextChanged" />
<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, ValidatesOnExceptions=True, NotifyOnValidationError=True}" InputScope="Number" Width="100" TextChanged="OnTextBoxTextChanged" />
<TextBlock Text=" x + " VerticalAlignment="Center" />
<TextBox Text="{Binding C, Mode=TwoWay, UpdateSourceTrigger=Explicit, ValidatesOnExceptions=True, NotifyOnValidationError=True}" InputScope="Number" Width="100" TextChanged="OnTextBoxTextChanged" />
<TextBlock Text=" = 0" VerticalAlignment="Center" /> </StackPanel> <StackPanel Name="result" Orientation="Horizontal" HorizontalAlignment="Center">
<TextBlock Text="{Binding Solution1.Real, Converter={StaticResource stringFormat}, ConverterParameter='x = {0:F3} '}" /> <TextBlock Text="+" Visibility="{Binding HasOneSolution, Converter={StaticResource booleanToVisibility}}" />
<TextBlock Text="±" Visibility="{Binding HasTwoSolutions, Converter={StaticResource booleanToVisibility}}" />
<TextBlock Text="{Binding Solution1.Imaginary, Converter={StaticResource stringFormat}, ConverterParameter=' {0:F3}i'}" /> </StackPanel> </StackPanel> </Grid>
|
You might also notice that I
completely revamped the display of the solutions. Rather than two TextBlock
elements to display two solutions, I use four TextBlock elements to display a single solution that
might contain a ± sign (Unicode 0x00B1).
The code-behind file
implements the updating and error handling:
Example 6. Silverlight
Project: QuadraticEquationSolver2 File: 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; } } }
|
Here’s a TextBox
indicating that an entry is incorrect:
If you had written a
quadratic equation solver for Windows Phone 7 prior to this article, the
screen might looked pretty much the same, but I suspect the program
would have been structured quite differently. I know that’s the case if
you wrote such a program for a code-only environment such as Windows
Forms.
Notice how converting the
program to a mostly XAML solution causes us to rethink the whole
architecture of the program. It’s always an interesting process to me
how our tools seems to govern how we solve problems. But in some ways
this is a good thing, and if you find yourself writing code specifically
to use in XAML (such as binding services and data converters), I think
you’re on the right track.