How to Use Simple Decision Making in Windows Phone 7

XAML is not a real programming language, here we see how XAML capable for making decisions.

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:

twe1.gif

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:

twe2.gif

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>


twe3.gif

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.

twe4.gif