Blograby

Weight Tracker (Charts & Graphs)

Are you watching your weight? Weight Tracker enables you to weigh in as often as you like and provides several ways to visualize your progress. It is a pivot-based app with three pivot items:

Although this is a pivot-based app, the purpose of this chapter is to explain how to include charts and graphs in your apps.

Charts & Graphs

The Silverlight team has created a very rich set of opensource controls for creating seven types of interactive charts: bar charts, line charts, pie charts, bubble charts, and more. These were created several years ago for desktop Silverlight and ship in the Silverlight Toolkit. At the time of this writing, these controls are not included in the Silverlight for Windows Phone Toolkit, so you must download them separately. Where you should download them, however, has been a source of confusion.

Currently, the best place to get the chart controls is directly from David Anson’s blog. David is the Microsoft developer responsible for these controls, and he was also kind enough to review this chapter. He provides “development releases” of the controls with the goal of exposing the latest functionality and bug fixes at a quicker pace than the official toolkit releases from Microsoft. His releases come with full source code as well as the compiled DLLs. The same source code can be compiled to work with Windows Phone, multiple versions of desktop Silverlight, as well as Windows Presentation Foundation (WPF).

If you’re uncomfortable with using this unofficial release, you can instead download the chart controls from the Silverlight Toolkit (the one intended for desktop computers). However, to make matters more confusing, you must not use the latest version of the Silverlight Toolkit. You can download the phonecompatible version of the Silverlight Toolkit (the November 2009 release for Silverlight 3) at http://bit.ly/sl3toolkit. The downside to using this version is that it is missing performance improvements and stacked series support (described later) that are present in David’s release.

Whichever route you take, you need to reference System.Windows.Controls. DataVisualization.Toolkit.dll, the assembly with all the chart-specific functionality. In David’s release, use the one under BinariesSilverlight3 inside the .zip file. If you install the November 2009 Silverlight Toolkit, you can find it in %ProgramFiles%Microsoft SDKs Silverlightv3.0ToolkitNov09Bin.

The chart controls from the Silverlight 4 Toolkit do not work on Windows Phone at the time of this writing!

Although the latest version of the Silverlight Toolkit contains the chart controls described in this chapter, you cannot currently use them. Windows Phone OS 7.0 ships with a custom version of Silverlight that is based on Silverlight 3, despite having a few additional features that were introduced in Silverlight 4. As a result,most Silverlight 2 or Silverlight 3 code can work on Windows Phone, but code written for Silverlight 4 may not. The Silverlight 4 version of the chart controls requires features that are not present on Windows Phone’s version of Silverlight, so attempting to use this version fails at run-time in hard-to-diagnose ways. Such incompatibilities between desktop Silverlight and Silverlight for Windows Phone should hopefully fade away in the future.

Regular Series

The primary element that enables charts is Chart from the System.Windows.Controls. DataVisualization.Charting namespace. To get different types of charts, you place different types of series in the Chart element. There are fifteen concrete series: seven regular ones and eight stacked ones. All of the regular ones are demonstrated in Table 29.1.

Each is shown with its default rendering under the light theme, which is decent (despite the use of phone-unfriendly gradients). The default rendering is not acceptable under the dark theme, however, as the legend text becomes white but the box’s background remains a light gradient.

From code-behind, each chart in Table 29.1 is given the same data context of three simple (X,Y) points as follows:

[code]

this.Chart.DataContext =
new Point[] { new Point(0, 2), new Point(1, 10), new Point(2, 6) };

[/code]

This way, each series can bind its ItemsSource property to this array with simple {Binding} syntax then specify each X property for its independent axis (the X axis in most chart types) and each Y property for its dependent axis (the Y axis in most chart types). You can directly manipulate a series without using data binding, but doing so from codebehind is a little awkward because you can’t access a series element by name.

TABLE 29.1 The Default Light-Theme Rendering of the Seven Nonstacked Chart Types, All with the Same Three-Point Data Source

Line Chart

Line Chart

[code]

<charting:Chart x:Name=”Chart”>
<charting:LineSeries
ItemsSource=”{Binding}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
</charting:Chart>

[/code]

Area Chart

Area Chart

[code]

<charting:Chart x:Name=”Chart”>
<charting:AreaSeries
ItemsSource=”{Binding}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
</charting:Chart>

[/code]

Bar Chart

Bar Chart

[code]

<charting:Chart x:Name=”Chart”>
<charting:BarSeries
ItemsSource=”{Binding}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
</charting:Chart>

[/code]

Column Chart

Column Chart

[code]

<charting:Chart x:Name=”Chart”>
<charting:ColumnSeries
ItemsSource=”{Binding}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
</charting:Chart>

[/code]

Scatter Chart

Scatter Chart

[code]

<charting:Chart x:Name=”Chart”>
<charting:ScatterSeries
ItemsSource=”{Binding}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
</charting:Chart>

[/code]

Bubble Chart

[code]

<charting:Chart x:Name=”Chart”>
<charting:BubbleSeries
ItemsSource=”{Binding}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
</charting:Chart>

[/code]

Pie Chart

Pie Chart

[code]

<charting:Chart x:Name=”Chart”>
<charting:PieSeries
ItemsSource=”{Binding}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
</charting:Chart>

[/code]

A single chart can contain multiple overlapping series, as with the following chart:

[code]

<charting:Chart x:Name=”Chart”>
<!– Data set #1 –>
<charting:AreaSeries IndependentValuePath=”X”
DependentValuePath=”Y”/>
<!– Data set #2 –>
<charting:AreaSeries IndependentValuePath=”X”
DependentValuePath=”Y”/>
</charting:Chart>

[/code]

Assigning each area series the following data produces the result in Figure 29.1 (under the light theme):

[code]

(this.Chart.Series[0] as AreaSeries).ItemsSource =
new Point[] { new Point(0, 2), new Point(1, 10), new Point(2, 6) };
(this.Chart.Series[1] as AreaSeries).ItemsSource =
new Point[] { new Point(0, 8), new Point(1, 1), new Point(2, 9) };

[/code]

FIGURE 29.1 Using two area series in the same chart.

Each series can even be a different type. Although it’s nonsensical in this case, Figure 29.2 combines all seven series from Table 29.1 into the same chart.

FIGURE 29.2 Using all seven types of nonstacked series in the same chart.

Stacked Series

In the Development Release 4 download of the chart controls, the first four series in Table 29.1 have two stacked counterparts: one that stacks the absolute values, and one that stacks relative values that always add up to 100%. Table 29.2 demonstrates these eight stacked series. They support multiple child series much like in Figure 29.1 but, as the name implies, each child get stacked rather than overlapped.

Each chart in Table 29.2 uses the following data context from code-behind:

[code]

this.Chart.DataContext = new Point[][] {
new Point[] { new Point(0, 2), new Point(1, 10), new Point(2, 6) }, // Series 1
new Point[] { new Point(0, 4), new Point(1, 5), new Point(2, 12) }, // Series 2
new Point[] { new Point(0, 8), new Point(1, 2.5), new Point(2, 3) } // Series 3
};

[/code]

A stacked series contains any number of series definitions. With this data context, the XAML snippets in Table 29.2 can bind three distinct series definitions to each Point[] element in the outermost array. When bound to an individual array of points, the assignments of IndependentValuePath and DependentValuePath work just like in the previous table.

TABLE 29.2 The Default Light-Theme Rendering of the Eight Stacked Chart Types, All with the Same Set of Three-Point Data Sources

Stacked Line Chart

Stacked Line Chart

[code]

<charting:Chart x:Name=”Chart”>
<charting:StackedLineSeries>
<charting:SeriesDefinition
ItemsSource=”{Binding [0]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
<charting:SeriesDefinition
ItemsSource=”{Binding [1]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
<charting:SeriesDefinition
ItemsSource=”{Binding [2]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
</charting:StackedLineSeries>
</charting:Chart>

[/code]

Stacked 100% Line Chart

Stacked 100% Line Chart

[code]

<charting:Chart x:Name=”Chart”>
<charting:Stacked100LineSeries>
<charting:SeriesDefinition
ItemsSource=”{Binding [0]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
<charting:SeriesDefinition
ItemsSource=”{Binding [1]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
<charting:SeriesDefinition
ItemsSource=”{Binding [2]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
</charting:Stacked100LineSeries>
</charting:Chart>

[/code]

Stacked Area Chart

Stacked Area Chart

[code]

<charting:Chart x:Name=”Chart”>
<charting:StackedAreaSeries>
<charting:SeriesDefinition
ItemsSource=”{Binding [0]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
<charting:SeriesDefinition
ItemsSource=”{Binding [1]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
<charting:SeriesDefinition
ItemsSource=”{Binding [2]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
</charting:StackedAreaSeries>
</charting:Chart>

[/code]

Stacked 100% Area Chart

Stacked 100% Area Chart

[code]

<charting:Chart x:Name=”Chart”>
<charting:Stacked100AreaSeries>
<charting:SeriesDefinition
ItemsSource=”{Binding [0]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
<charting:SeriesDefinition
ItemsSource=”{Binding [1]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
<charting:SeriesDefinition
ItemsSource=”{Binding [2]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
</charting:Stacked100AreaSeries>
</charting:Chart>

[/code]

Stacked Bar Chart

Stacked Bar Chart

[code]

<charting:Chart x:Name=”Chart”>
<charting:StackedBarSeries>
<charting:SeriesDefinition
ItemsSource=”{Binding [0]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
<charting:SeriesDefinition
ItemsSource=”{Binding [1]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
<charting:SeriesDefinition
ItemsSource=”{Binding [2]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
</charting:StackedBarSeries>
</charting:Chart>

[/code]

Stacked 100% Bar Chart

Stacked 100% Bar Chart

[code]

<charting:Chart x:Name=”Chart”>
<charting:Stacked100BarSeries>
<charting:SeriesDefinition
ItemsSource=”{Binding [0]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
<charting:SeriesDefinition
ItemsSource=”{Binding [1]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
<charting:SeriesDefinition
ItemsSource=”{Binding [2]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
</charting:Stacked100BarSeries>
</charting:Chart>

[/code]

Stacked Column Chart

Stacked Column Chart

[code]

<charting:StackedColumnSeries>
<charting:SeriesDefinition
ItemsSource=”{Binding [0]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
<charting:SeriesDefinition
ItemsSource=”{Binding [1]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
<charting:SeriesDefinition
ItemsSource=”{Binding [2]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
</charting:StackedColumnSeries>
</charting:Chart>

[/code]

Stacked 100% Column Chart

Stacked 100% Column Chart

[code]

<charting:Chart x:Name=”Chart”>
<charting:Stacked100ColumnSeries>
<charting:SeriesDefinition
ItemsSource=”{Binding [0]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
<charting:SeriesDefinition
ItemsSource=”{Binding [1]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
<charting:SeriesDefinition
ItemsSource=”{Binding [2]}”
IndependentValuePath=”X”
DependentValuePath=”Y”/>
</charting:Stacked100ColumnSeries>
</charting:Chart>

[/code]

As you can see, getting reasonablelooking charts to render is fairly easy. The chart scales its axes appropriately, and even staggers axis labels if necessary (seen in Table 29.1). It applies different colors to the data automatically and has a lot of other intelligence not shown here. Chart, as well as each series object, also exposes numerous properties for formatting the chart area, legend, title, axes, data points, and more. The amount of possible customization is vast, although figuring out how to get your desired customizations (even simple-sounding things like hiding the legend or changing data colors) can be quite tricky. In the next section, Weight Tracker demonstrates how to perform several customizations on line charts and pie charts.

The Main Page

Besides the application bar and status bar, the main page contains the three-item pivot. These three items, described at the beginning of this chapter, are shown in Figure 29.3.

FIGURE 29.3 The three pivot items: list, graph, and progress.

The User Interface

Listing 29.1 contains the XAML for the main page.

LISTING 29.1 MainPage.xaml—The User Interface for Weight Tracker’s Main Page

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
xmlns:controls=”clr-namespace:Microsoft.Phone.Controls;
➥assembly=Microsoft.Phone.Controls”
xmlns:toolkit=”clr-namespace:Microsoft.Phone.Controls;
➥assembly=Microsoft.Phone.Controls.Toolkit”
xmlns:charting=”clr-namespace:
➥System.Windows.Controls.DataVisualization.Charting;
➥assembly=System.Windows.Controls.DataVisualization.Toolkit”
xmlns:chartingprimitives=”clr-namespace:
➥System.Windows.Controls.DataVisualization.Charting.Primitives;
➥assembly=System.Windows.Controls.DataVisualization.Toolkit”
xmlns:datavis=”clr-namespace:System.Windows.Controls.DataVisualization;
➥assembly=System.Windows.Controls.DataVisualization.Toolkit”
xmlns:local=”clr-namespace:WindowsPhoneApp”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”PortraitOrLandscape” shell:SystemTray.IsVisible=”True”>
<!– The application bar, with two buttons and a menu item –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar>
<shell:ApplicationBarIconButton Text=”instructions”
IconUri=”/Shared/Images/appbar.instructions.png”
Click=”InstructionsButton_Click”/>
<shell:ApplicationBarIconButton Text=”settings”
IconUri=”/Shared/Images/appbar.settings.png”
Click=”SettingsButton_Click”/>
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text=”about” Click=”AboutMenuItem_Click”/>
</shell:ApplicationBar.MenuItems>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<phone:PhoneApplicationPage.Resources>
<!– A custom style for Chart. It’s the default style from the toolkit, but
with no title/legend, reduced margins/padding, and fewer borders. –>
<Style x:Key=”ChartStyle” TargetType=”charting:Chart”>

</Style>
</phone:PhoneApplicationPage.Resources>
<!– The pivot is the root element –>
<controls:Pivot x:Name=”Pivot” Title=”WEIGHT TRACKER”
SelectionChanged=”Pivot_SelectionChanged”>
<!– Item 1: list –>
<controls:PivotItem Header=”list”>
<Grid Margin=”0,-24,0,0”>
<!– A translucent foreground-colored scale image behind the list –>
<Rectangle Opacity=”.2” Margin=”0,0,0,12” VerticalAlignment=”Bottom”
Width=”180” Height=”240” Fill=”{StaticResource PhoneForegroundBrush}”>
<Rectangle.OpacityMask>
<ImageBrush ImageSource=”Images/scale.png”/>
</Rectangle.OpacityMask>
</Rectangle>
<!– The editable list, implemented as a user control –>
<local:WeighInEditableList x:Name=”EditableList” Collection=”{Binding}”/>
</Grid>
</controls:PivotItem>
<!– Item 2: graph –>
<controls:PivotItem Header=”graph”>
<Grid Margin=”0,-24,0,0”>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition/>
</Grid.RowDefinitions>
<!– The start date / end date pickers –>
<TextBlock Text=”Start date” Margin=”11,0,0,-5”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<toolkit:DatePicker x:Name=”StartGraphDatePicker” Grid.Row=”1”
ValueChanged=”GraphDatePicker_ValueChanged” local:Tilt.IsEnabled=”True”/>
<TextBlock Text=”End date” Grid.Column=”1” Margin=”11,0,0,-5”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<toolkit:DatePicker x:Name=”EndGraphDatePicker” Grid.Row=”1”
Grid.Column=”1” ValueChanged=”GraphDatePicker_ValueChanged”
local:Tilt.IsEnabled=”True”/>
<!– The line + scatter chart –>
<charting:Chart x:Name=”Chart” Style=”{StaticResource ChartStyle}”
Grid.ColumnSpan=”2” Grid.Row=”2”>
<!– Change the background of the plot area –>
<charting:Chart.PlotAreaStyle>
<Style TargetType=”Grid”>
<Setter Property=”Background”
Value=”{StaticResource PhoneBackgroundBrush}”/>
</Style>
</charting:Chart.PlotAreaStyle>
<!– An explicit X axis, so it can be customized –>
<charting:Chart.Axes>
<charting:DateTimeAxis ShowGridLines=”True” Orientation=”X”>
<charting:DateTimeAxis.AxisLabelStyle>
<Style TargetType=”charting:DateTimeAxisLabel”>
<Setter Property=”StringFormat” Value=”{}{0:M/d}” />
</Style>
</charting:DateTimeAxis.AxisLabelStyle>
</charting:DateTimeAxis>
</charting:Chart.Axes>
<!– The line series, for weigh-in data –>
<charting:LineSeries ItemsSource=”{Binding}”
IndependentValuePath=”Date”
DependentValuePath=”Weight”>
<!– A custom style for the line –>
<charting:LineSeries.PolylineStyle>
<Style TargetType=”Polyline”>
<Setter Property=”StrokeThickness” Value=”3”/>
</Style>
</charting:LineSeries.PolylineStyle>
<!– A custom style for data points –>
<charting:LineSeries.DataPointStyle>
<!– A copy of the default style, but with a new background,
border, width, height, and removal of the radial gradient –>
<Style TargetType=”charting:LineDataPoint”>
<Setter Property=”Background”
Value=”{StaticResource PhoneAccentBrush}”/>
<Setter Property=”BorderBrush”
Value=”{StaticResource PhoneForegroundColor}”/>
<Setter Property=”Width” Value=”10” />
<Setter Property=”Height” Value=”10” />

</Style>
</charting:LineSeries.DataPointStyle>
</charting:LineSeries>
<!– The scatter series, for goal weights –>
<charting:ScatterSeries IndependentValuePath=”Date”
DependentValuePath=”Weight”>
<!– A custom style for data points –>
<charting:ScatterSeries.DataPointStyle>
<Style TargetType=”charting:ScatterDataPoint”>
<!– Width and Height need to be set for the point
to render properly –>
<Setter Property=”Width” Value=”26”/>
<Setter Property=”Height” Value=”25”/>
<Setter Property=”Template”>
<Setter.Value>
<ControlTemplate TargetType=”charting:ScatterDataPoint”>
<!– A star image –>
<Rectangle Fill=”#FFC700”>
<Rectangle.OpacityMask>
<ImageBrush ImageSource=”Images/star.png”/>
</Rectangle.OpacityMask>
</Rectangle>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</charting:ScatterSeries.DataPointStyle>
</charting:ScatterSeries>
</charting:Chart>
</Grid>
</controls:PivotItem>
<!– Item 3: progress –>
<controls:PivotItem Header=”progress”>
<Grid>
<TextBlock x:Name=”NoDataTextBlock” Margin=”22,17,0,0”
Style=”{StaticResource PhoneTextGroupHeaderStyle}”>
You don’t have any goals set.<LineBreak/>Set one on the settings page.
</TextBlock>
<ScrollViewer x:Name=”DataDashboard” Margin=”0,-24,0,0”>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<!– The first pie chart –>
<charting:Chart x:Name=”WeightPieChart” Grid.Row=”1”
Style=”{StaticResource ChartStyle}”>
<!– Change the background of the plot area –>
<charting:Chart.PlotAreaStyle>
<Style TargetType=”Grid”>
<Setter Property=”Background”
Value=”{StaticResource PhoneBackgroundBrush}”/>
</Style>
</charting:Chart.PlotAreaStyle>
<!– The pie series –>
<charting:PieSeries ItemsSource=”{Binding}”>
<!– A custom two-color palette: the accent color for the first
piece, and the background color for the second piece –>
<charting:PieSeries.Palette>
<datavis:ResourceDictionaryCollection>
<!– Style for the first data point –>
<ResourceDictionary>
<Style x:Key=”DataPointStyle” TargetType=”Control”>
<Setter Property=”BorderBrush”
Value=”{StaticResource PhoneBackgroundBrush}”/>
<Setter Property=”Background”
Value=”{StaticResource PhoneAccentBrush}”/>
</Style>
</ResourceDictionary>
<!– Style for the second data point –>
<ResourceDictionary>
<Style x:Key=”DataPointStyle” TargetType=”Control”>
<Setter Property=”BorderBrush”
Value=”{StaticResource PhoneBackgroundBrush}”/>
<Setter Property=”Background”
Value=”{StaticResource PhoneBackgroundBrush}”/>
</Style>
</ResourceDictionary>
</datavis:ResourceDictionaryCollection>
</charting:PieSeries.Palette>
</charting:PieSeries>
</charting:Chart>
<!– The second pie chart –>
<charting:Chart x:Name=”TimePieChart” Grid.Row=”1” Grid.Column=”1”
Style=”{StaticResource ChartStyle}”>
… The contents are identical to the preceding pie chart …
</charting:Chart>
<!– Lots of text blocks –>
<TextBlock x:Name=”WeightLossPercentTextBlock” Text=”Weight loss”
Margin=”4” HorizontalAlignment=”Center”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<TextBlock x:Name=”TimePercentTextBlock” Grid.Column=”1” Text=”Time”
Margin=”4” HorizontalAlignment=”Center”
Foreground=”{StaticResource PhoneSubtleBrush}”/>
<TextBlock x:Name=”SummaryTextBlock” Grid.Row=”2” Grid.ColumnSpan=”2”
HorizontalAlignment=”Center” VerticalAlignment=”Top”
FontFamily=”Segoe WP Black” Margin=”0,0,0,8”
Style=”{StaticResource PhoneTextLargeStyle}”/>
<TextBlock Grid.Row=”3” Text=”Weight lost”
HorizontalAlignment=”Center” Style=”{StaticResource LabelStyle}”/>
<TextBlock x:Name=”WeightLostTextBlock” Grid.Row=”4”
HorizontalAlignment=”Center”
Style=”{StaticResource PhoneTextExtraLargeStyle}”/>
<TextBlock Grid.Row=”5” Text=”Weight to lose”
HorizontalAlignment=”Center” Style=”{StaticResource LabelStyle}”/>
<TextBlock x:Name=”WeightRemainingTextBlock” Grid.Row=”6”
HorizontalAlignment=”Center” Margin=”0,0,0,24” FontSize=”60”/>
<TextBlock Grid.Row=”3” Grid.Column=”1” Text=”Days elapsed”
HorizontalAlignment=”Center” Style=”{StaticResource LabelStyle}”/>
<TextBlock x:Name=”DaysElapsedTextBlock” Grid.Row=”4” Grid.Column=”1”
HorizontalAlignment=”Center”
Style=”{StaticResource PhoneTextExtraLargeStyle}”/>
<TextBlock Grid.Row=”5” Grid.Column=”1” Text=”Days remaining”
HorizontalAlignment=”Center”
Style=”{StaticResource LabelStyle}”/>
<TextBlock x:Name=”DaysRemainingTextBlock” Grid.Row=”6”
Grid.Column=”1” HorizontalAlignment=”Center”
Margin=”0,0,0,24” FontSize=”60”/>
</Grid>
</ScrollViewer>
</Grid>
</controls:PivotItem>
</controls:Pivot>
</phone:PhoneApplicationPage>

[/code]

To demonstrate their impact, Figure 29.4 displays this app’s line and scatter chart with various amounts of customizations removed. The second image shows the result of simply removing Style=”{StaticResource ChartStyle}” from the chart. The first image is the result of whittling the chart down to the following simplest version that still works without changing any additional code:

[code]

<!– The line + scatter chart –>
<charting:Chart x:Name=”Chart”
Grid.ColumnSpan=”2” Grid.Row=”2”>
<!– An explicit X axis, so its range can be restricted from code-behind –>
<charting:Chart.Axes>
<charting:DateTimeAxis Orientation=”X”/>
</charting:Chart.Axes>
<!– The line series, for weigh-in data –>
<charting:LineSeries ItemsSource=”{Binding}”
IndependentValuePath=”Date”
DependentValuePath=”Weight”/>
<!– The scatter series, for goal weights –>
<charting:ScatterSeries IndependentValuePath=”Date”
DependentValuePath=”Weight”/>
</charting:Chart>

[/code]

FIGURE 29.4 Some of the chart customizations are done by setting exposed properties, whereas others are done by changing the control template inside ChartStyle.

The Code-Behind

Listing 29.2 contains the code-behind for the main page, which makes the most of observable collections to keep all three pivot items up-to-date. This app manages two observable collections—one for the list of weigh-ins, and one for the list of goal weights. They are defined as follows in Settings.cs, along with a setting for remembering the main chart’s selected start date:

[code]

public static class Settings
{
public static readonly Setting<DateTime?> GraphStartDate =
new Setting<DateTime?>(“GraphStartDate”, null);
public static readonly Setting<WeighInCollection> WeighInList =
new Setting<WeighInCollection>(“WeighInList”, new WeighInCollection());
public static readonly Setting<WeighInCollection> GoalList =
new Setting<WeighInCollection>(“GoalList”, new WeighInCollection());
}

[/code]

LISTING 29.2 MainPage.xaml.cs—The Code-Behind for Weight Tracker’s Main Page

[code]

using System;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.DataVisualization.Charting;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
public MainPage()
{
InitializeComponent();
// Update the start/end dates of the main chart (done here so it
// doesn’t interfere with navigating back from the date pickers)
if (Settings.WeighInList.Value.Count + Settings.GoalList.Value.Count > 0)
{
// Restore the start date to the previously-used value,
// otherwise use the earliest date with data
if (Settings.GraphStartDate.Value != null)
this.StartGraphDatePicker.Value = Settings.GraphStartDate.Value;
else
this.StartGraphDatePicker.Value = GetEarliestDataPoint().Date;
// Don’t restore any customizations to the end date. Set it to the
// date of the last weigh-in or goal
this.EndGraphDatePicker.Value = GetLatestDataPoint().Date;
}
// Update the progress dashboard (the third pivot item)
UpdateProgress();
// Respond to changes in the two observable collections
Settings.WeighInList.Value.CollectionChanged += CollectionChanged;
Settings.GoalList.Value.CollectionChanged += CollectionChanged;
// Set the weigh-in list as the default data source for everything
this.DataContext = Settings.WeighInList.Value;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Fill out the text box with the most recent weight
if (this.EditableList.Text.Length == 0 &&
Settings.WeighInList.Value.Count > 0)
this.EditableList.Text = Settings.WeighInList.Value[0].Weight.ToString();
// When reactivated, restore the pivot selection
if (this.State.ContainsKey(“PivotSelectedIndex”))
this.Pivot.SelectedIndex = (int)this.State[“PivotSelectedIndex”];
// Set the goal list as the source for the scatter series on the main chart
(this.Chart.Series[1] as DataPointSeries).ItemsSource =
Settings.GoalList.Value;
}
void Pivot_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// Remember the current pivot item for reactivation only
this.State[“PivotSelectedIndex”] = this.Pivot.SelectedIndex;
}
// Returns the earliest data point from either of the two lists
WeighIn GetEarliestDataPoint()
{
WeighIn earliest = null;
// Both lists are sorted in reverse chronological order
if (Settings.WeighInList.Value.Count > 0)
earliest = Settings.WeighInList.Value[Settings.WeighInList.Value.Count-1];
if (Settings.GoalList.Value.Count > 0)
{
WeighIn earliestGoal =
Settings.GoalList.Value[Settings.GoalList.Value.Count-1];
if (earliest == null || earliestGoal.Date < earliest.Date)
earliest = earliestGoal;
}
return earliest;
}
// Returns the latest data point from either of the two lists
WeighIn GetLatestDataPoint()
{
WeighIn latest = null;
// Both lists are sorted in reverse chronological order
if (Settings.WeighInList.Value.Count > 0)
latest = Settings.WeighInList.Value[0];
if (Settings.GoalList.Value.Count > 0)
{
WeighIn latestGoal = Settings.GoalList.Value[0];
if (latest == null || latestGoal.Date > latest.Date)
latest = latestGoal;
}
return latest;
}
// Called when either of the two observable collections changes
void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
WeighIn earliestDataPoint = GetEarliestDataPoint();
WeighIn latestDataPoint = GetLatestDataPoint();
// Potentially update the range of the main chart
if (earliestDataPoint != null && latestDataPoint != null)
{
// Check if a new earliest date was added. Only update start date if so.
if ((sender == Settings.WeighInList.Value &&
e.NewStartingIndex == Settings.WeighInList.Value.Count-1) ||
(sender == Settings.GoalList.Value &&
e.NewStartingIndex == Settings.GoalList.Value.Count-1))
this.StartGraphDatePicker.Value = earliestDataPoint.Date;
// Ensure the end date matches the end of the data
this.EndGraphDatePicker.Value = latestDataPoint.Date;
}
// Update the progress dashboard (the third pivot item)
UpdateProgress();
}
void UpdateProgress()
{
// Refresh all the data on this pivot item
this.NoDataTextBlock.Visibility = Settings.GoalList.Value.Count > 0 ?
Visibility.Collapsed : Visibility.Visible;
this.DataDashboard.Visibility = Settings.GoalList.Value.Count > 0 ?
Visibility.Visible : Visibility.Collapsed;
if (Settings.GoalList.Value.Count == 0)
return;
WeighIn earliestGoal =
Settings.GoalList.Value[Settings.GoalList.Value.Count-1];
WeighIn latestGoal = Settings.GoalList.Value[0];
int daysRemaining = 0;
double weightRemaining = latestGoal.Weight;
int daysElapsed = 0;
double weightLost = 0;
if (Settings.WeighInList.Value.Count > 0)
{
WeighIn earliestWeighIn =
Settings.WeighInList.Value[Settings.WeighInList.Value.Count-1];
WeighIn latestWeighIn = Settings.WeighInList.Value[0];
daysRemaining = (latestGoal.Date – latestWeighIn.Date).Days;
daysElapsed = (latestWeighIn.Date – earliestWeighIn.Date).Days;
weightLost = earliestWeighIn.Weight – latestWeighIn.Weight;
weightRemaining = latestWeighIn.Weight – latestGoal.Weight;
}
double weightPercent = weightLost / (weightRemaining + weightLost);
double timePercent = (double)daysElapsed / (daysRemaining + daysElapsed);
// Update text
this.WeightLossPercentTextBlock.Text =
“Weight loss: “ + (weightPercent * 100).ToString(“N0”) + “%”;
this.TimePercentTextBlock.Text =
“Time: “ + (timePercent * 100).ToString(“N0”) + “%”;
this.DaysElapsedTextBlock.Text = daysElapsed.ToString(“N0”);
this.DaysRemainingTextBlock.Text = daysRemaining.ToString(“N0”);
this.WeightLostTextBlock.Text = weightLost.ToString(“0.#”);
this.WeightRemainingTextBlock.Text = weightRemaining.ToString(“0.#”);
if (weightPercent > timePercent)
this.SummaryTextBlock.Text = “AHEAD OF SCHEDULE!”;
else if (weightPercent < timePercent)
this.SummaryTextBlock.Text = “FALLING BEHIND”;
else
this.SummaryTextBlock.Text = “ON TRACK”;
// Set the data for the two pie charts
this.WeightPieChart.DataContext =
new double[] { weightLost, weightRemaining };
this.TimePieChart.DataContext =
new int[] { daysElapsed, daysRemaining };
}
void GraphDatePicker_ValueChanged(object sender,
DateTimeValueChangedEventArgs e)
{
// Minimum must be <= maximum
if (this.StartGraphDatePicker.Value > this.EndGraphDatePicker.Value)
this.StartGraphDatePicker.Value = this.EndGraphDatePicker.Value;
// Update the range of the X axis
(this.Chart.Axes[0] as DateTimeAxis).Minimum =
this.StartGraphDatePicker.Value;
(this.Chart.Axes[0] as DateTimeAxis).Maximum =
this.EndGraphDatePicker.Value;
// Remember this new graph start date
Settings.GraphStartDate.Value = this.StartGraphDatePicker.Value;
}
// Application bar handlers
void InstructionsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/InstructionsPage.xaml”,
UriKind.Relative));
}
void SettingsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/SettingsPage.xaml”,
UriKind.Relative));
}
void AboutMenuItem_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(
“/Shared/About/AboutPage.xaml?appName=Weight Tracker”, UriKind.Relative));
}
}
}

[/code]

The Settings Page

FIGURE 29.5 The settings page enables editing a list of goal weights by hosting the same user control as the main page.

The settings page, shown in Figure 29.5, enables users to view, add, and delete date-based weight goals exactly how they view, add, and delete weigh-ins on the main page. That’s because it leverages the same WeighInEditableList user control. The page also contains a delete button for clearing all weigh-ins and all goals in bulk.

Because most of the functionality is provided by the WeighInEditableList user control, the implementation of the settings page is short and straightforward. Listing 29.3 contains its XAML and Listing 29.4 contains its codebehind. WeighInEditableList’s IsGoalList property being set to true is what makes a star appear next to each weight, rather than the up/down arrows seen on the main page.

LISTING 29.3 SettingsPage.xaml—The User Interface for Weight Tracker’s Settings Page

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.SettingsPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
xmlns:toolkit=”clr-namespace:Microsoft.Phone.Controls;
➥assembly=Microsoft.Phone.Controls.Toolkit”
xmlns:local=”clr-namespace:WindowsPhoneApp”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”PortraitOrLandscape” shell:SystemTray.IsVisible=”True”>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”*”/>
<RowDefinition Height=”Auto”/>
</Grid.RowDefinitions>
<!– The standard settings header –>
<StackPanel Grid.Row=”0” Style=”{StaticResource PhoneTitlePanelStyle}”>
<TextBlock Text=”SETTINGS” Style=”{StaticResource PhoneTextTitle0Style}”/>
<TextBlock Text=”weight tracker”
Style=”{StaticResource PhoneTextTitle1Style}”/>
</StackPanel>
<TextBlock Grid.Row=”1” Margin=”24,12,24,24”
Text=”Enter one or more weight goals.”/>
<!– A translucent foreground-colored star image behind the list –>
<Rectangle Grid.Row=”2” Opacity=”.2” Margin=”0,0,0,12” Width=”240”
Height=”240” VerticalAlignment=”Bottom”
Fill=”{StaticResource PhoneForegroundBrush}”>
<Rectangle.OpacityMask>
<ImageBrush ImageSource=”Images/bigStar.png”/>
</Rectangle.OpacityMask>
</Rectangle>
<!– The editable list user control –>
<local:WeighInEditableList Grid.Row=”2” Collection=”{Binding}”
IsGoalList=”True” Margin=”12,0”/>
<!– Delete button –>
<Button x:Name=”DeleteButton” Grid.Row=”3” Content=”delete all data”
local:Tilt.IsEnabled=”True” Click=”DeleteButton_Click”/>
</Grid>
</phone:PhoneApplicationPage>

[/code]

LISTING 29.4 SettingsPage.xaml.cs—The Code-Behind for Weight Tracker’s Settings Page

[code]

using System.Windows;
using Microsoft.Phone.Controls;
namespace WindowsPhoneApp
{
public partial class SettingsPage : PhoneApplicationPage
{
public SettingsPage()
{
InitializeComponent();
// This time, the data source for the editable list is the goal list
this.DataContext = Settings.GoalList.Value;
}
void DeleteButton_Click(object sender, RoutedEventArgs e)
{
if (MessageBox.Show(
“Are you sure you want to clear all your weigh-ins and goals?”, “Delete”,
MessageBoxButton.OKCancel) == MessageBoxResult.OK)
{
Settings.GoalList.Value.Clear();
Settings.WeighInList.Value.Clear();
}
}
}
}

[/code]

The Finished Product

Exit mobile version