This chapter
is taken from book "Programming Windows Phone 7" by Charles Petzold published by
Microsoft press.
http://www.charlespetzold.com/phone/index.html
XAML is not a real programming language. It doesn't include anything like if
statements. XAML isn't capable of
making decisions. But that doesn't mean we can't try.
Here's a new class named
TwelveHourClock that derives from Clock and the
Clock class used the straight Hour property from DateTime, which is a 24-hour
clock value. You might instead want a 12-hour clock and display the text "AM" or
"PM" to indicate the morning or afternoon. Normally you can do that by
formatting the time but suppose you want to be very flexible about how you
display the AM and PM information-perhaps you'd prefer to display the text "in
the morning" or "in the afternoon"-and you want to do it in XAML.
namespace
Petzold.Phone.Silverlight
{
public class
TwelveHourClock :
Clock
{
int hour12;
bool isam, ispm;
public int
Hour12
{
protected
set
{
if (value
!= hour12)
{
hour12 = value;
OnPropertyChanged(new
PropertyChangedEventArgs("Hour12"));
}
}
get
{
return hour12;
}
}
public bool
IsAm
{
protected
set
{
if (value
!= isam)
{
isam = value;
OnPropertyChanged(new
PropertyChangedEventArgs("IsAm"));
}
}
get
{
return isam;
}
}
public bool
IsPm
{
protected
set
{
if (value
!= ispm)
{
ispm = value;
OnPropertyChanged(new
PropertyChangedEventArgs("IsPm"));
}
}
get
{
return ispm;
}
}
protected
override void OnPropertyChanged(PropertyChangedEventArgs
args)
{
if (args.PropertyName ==
"Hour")
{
Hour12 = (Hour - 1) % 12 + 1;
IsAm = Hour < 12;
IsPm = !IsAm;
}
base.OnPropertyChanged(args);
}
}
}
The TwelveHourClock class defines three new
properties, all triggering PropertyChanged events. These are Hour12 and two
Boolean properties, IsAm and IsPm. The override of OnPropertyChanged checks if
the property being changed is Hour and, if so, calculates new values for these
three properties, which themselves cause calls to OnPropertyChanged.
This XAML has separate text strings for morning and afternoon, but at any
time only one of them should be displayed depending on whether IsAm or IsPm
is true. How is such a thing even
possible?
Another converter is required, and this is also a
converter that you'll use quite often. It's called a
BooleanToVisibilityConverter and it assumes that
the source value is a Boolean and the target is a property of type Visibility:
namespace
Petzold.Phone.Silverlight
{
public class
BooleanToVisibilityConverter :
IValueConverter
{
public object
Convert(object value,
Type targetType,
object parameter,
CultureInfo culture)
{
return (bool)value
? Visibility.Visible :
Visibility.Collapsed;
}
public object
ConvertBack(object value,
Type targetType,
object
parameter, CultureInfo culture)
{
return (Visibility)value
== Visibility.Visible;
}
}
}
Now bind the Visibility properties of the final two TextBlock elements to the
IsAm and IsPm properties using the BooleanToVisibilityConverter. Here's the
markup from the project AmOrPm:
<Grid
x:Name="ContentPanel"
Grid.Row="1"
Margin="12,0,12,0">
<StackPanel
DataContext="{StaticResource
clock12}"
Orientation="Horizontal"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock
Text="It's after " />
<TextBlock
Text="{Binding
Hour}" />
<TextBlock
Text=" in the morning."
Visibility="{Binding
IsAm,
Converter={StaticResource
booleanToVisibility}}" />
<TextBlock
Text=" in the afternoon."
Visibility="{Binding
IsPm,
Converter={StaticResource
booleanToVisibility}}"/>
</StackPanel>
</Grid>
And it works:
Converters with Properties
It's not unreasonable to create a data-binding converter that is so
specialized or so weird that it's only good for one particular application. For
example, here's a class called DecimalBitToBrushConverter. This converter
includes two public properties named ZeroBitBrush and OneBitBrush.
namespace
BinaryClock
{
public class
DecimalBitToBrushConverter :
IValueConverter
{
public Brush
ZeroBitBrush { set; get;
}
public Brush
OneBitBrush { set; get;
public object
Convert(object value,
Type targetType,
object parameter,
CultureInfo culture)
{
int number = (int)value;
int bit =
Int32.Parse(parameter
as string);
int digit = number / PowerOfTen(bit
/ 4) % 10;
return ((digit & (1 << (bit % 4)))
== 0) ? ZeroBitBrush : OneBitBrush;
}
public object
ConvertBack(object value,
Type targetType,
object
parameter, CultureInfo culture)
{
return null;
}
int PowerOfTen(int
exp)
{
int value = 1;
for (int
i = 0; i < exp; i++)
value *= 10;
return value;
}
}
}
I use this converter in a project called BinaryClock. The converter is
referenced in a UserControl derivative called BinaryNumberRow. Notice how the
two public properties of DecimalBitToBrushConverter are set right in the
Resources collection, which also includes a Style for the Ellipse.
<UserControl
x:Class="BinaryClock.BinaryNumberRow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:petzold="clr-namespace:Petzold.Phone.Silverlight;assembly=Petzold.Phone.Silverlight"
xmlns:local="clr-namespace:BinaryClock">
<UserControl.Resources>
<Style
x:Key="ellipseStyle"
TargetType="Ellipse">
<Setter
Property="Width"
Value="48"
/>
<Setter
Property="Height"
Value="48"
/>
<Setter
Property="Stroke"
Value="{StaticResource
PhoneForegroundBrush}"
/>
<Setter
Property="StrokeThickness"
Value="2"
/>
</Style>
<local:DecimalBitToBrushConverter
x:Key="converter"
ZeroBitBrush="{x:Null}"
OneBitBrush="Red"
/>
</UserControl.Resources>
<petzold:UniformStack
Orientation="Horizontal">
<Ellipse
Style="{StaticResource
ellipseStyle}"
Fill="{Binding
Converter={StaticResource
converter},
ConverterParameter=6}"
/>
<Ellipse
Style="{StaticResource
ellipseStyle}"
Fill="{Binding
Converter={StaticResource
converter},
ConverterParameter=5}"
/>
<Ellipse
Style="{StaticResource
ellipseStyle}"
Fill="{Binding
Converter={StaticResource
converter},
ConverterParameter=4}"
/>
<Ellipse
Style="{StaticResource
ellipseStyle}"
Stroke="{x:Null}"
/>
<Ellipse
Style="{StaticResource
ellipseStyle}"
Fill="{Binding
Converter={StaticResource
converter},
ConverterParameter=3}"
/>
<Ellipse
Style="{StaticResource
ellipseStyle}"
Fill="{Binding
Converter={StaticResource
converter},
ConverterParameter=2}"
/>
<Ellipse
Style="{StaticResource
ellipseStyle}"
Fill="{Binding
Converter={StaticResource
converter},
ConverterParameter=1}"
/>
<Ellipse
Style="{StaticResource
ellipseStyle}"
Fill="{Binding
Converter={StaticResource
converter},
ConverterParameter=0}"
/>
</petzold:UniformStack>
</UserControl>
The content area contains a vertically centered StackPanel with three
instances of BinaryNumberRow:
<Grid
x:Name="ContentPanel"
Grid.Row="1"
Margin="12,0,12,0">
<StackPanel
DataContext="{StaticResource
clock12}"
VerticalAlignment="Center">
<local:BinaryNumberRow
DataContext="{Binding
Hour12}"
Margin="0 12" />
<local:BinaryNumberRow
DataContext="{Binding
Minute}"
Margin="0 12" />
<local:BinaryNumberRow
DataContext="{Binding
Second}"
Margin="0 12" />
</StackPanel>
</Grid>
The result, of course, is a binary clock:
Give and Take
The two binding services you've seen so far simply provide information. You
can also create bindings in XAML that deliver data to the binding service and
get back a result. As a very simple demonstration, let's look at a binding
service that performs the momentous feat of adding two numbers together. I call
it Adder.
using
System.ComponentModel;
namespace
Petzold.Phone.Silverlight
{
public class
Adder :
INotifyPropertyChanged
{
public event
PropertyChangedEventHandler PropertyChanged;
double augend = 0;
double addend = 0;
double sum = 0;
public double
Augend
{
set
{
if (augend !=
value)
{
augend = value;
OnPropertyChanged(new
PropertyChangedEventArgs("Augend"));
CalculateNewSum();
}
}
get
{
return augend;
}
}
public double
Addend
{
set
{
if (addend !=
value)
{
addend = value;
OnPropertyChanged(new
PropertyChangedEventArgs("Addend"));
CalculateNewSum();
}
}
get
{
return addend;
}
}
public double
Sum
{
protected
set
{
if (sum !=
value)
{
sum = value;
OnPropertyChanged(new
PropertyChangedEventArgs("Sum"));
}
}
get
{
return sum;
}
}
void CalculateNewSum()
{
Sum = Augend + Addend;
}
protected
virtual void OnPropertyChanged(PropertyChangedEventArgs
args)
{
if (PropertyChanged !=
null)
PropertyChanged(this, args);
}
}
}
In the content area, two Slider elements are positioned at the top and
bottom, and a TextBlock occupies the larger interior:
<Grid
x:Name="ContentPanel"
Grid.Row="1"
Margin="12,0,12,0"
DataContext="{Binding
Source={StaticResource
adder}}">
<Grid.RowDefinitions>
<RowDefinition
Height="Auto" />
<RowDefinition
Height="*" />
<RowDefinition
Height="Auto" />
</Grid.RowDefinitions>
<Slider
Grid.Row="0"
Minimum="-100"
Maximum="100"
Margin="24"
Value="{Binding
Augend,
Mode=TwoWay}" />
<Slider
Grid.Row="2"
Minimum="-100"
Maximum="100"
Margin="24"
Value="{Binding
Addend,
Mode=TwoWay}" />
<TextBlock
Grid.Row="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="48"
Text="{Binding
Sum,
Converter={StaticResource
stringFormat},
ConverterParameter=' {0:F2}
'}" />
</Grid>
TextBox Binding Updates
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, 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:
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.
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 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.)
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));
}
}
}
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.
<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.