Partager via


PetrolTracker Code-Behind

patterns & practices Developer Center

You Will Learn

  • How a view accesses its underlying model.
  • How an event handler performs an operation in response to a user action.
  • How a view is updated in response to changes in the underlying model.
  • How to save and restore page state.

Applies To

  • Silverlight for Windows Phone OS 7.1
On this page: Download:
The Contents of the PetrolTracker.CodeBehind Project | Accessing the Model | Application Launching Event | The User Interface | VehicleHistory | AddFillUp | Settings | Saving and Restoring Page State | Summary Download code samples

The PetrolTracker code-behind application enables a user to track the petrol consumption of three sample cars. The application forwards UI gestures to the code-behind as events. When an event is raised, its corresponding event handler in the code-behind is executed. Each handler performs the required operation and then sets the state of the page. Setting the state of the page is accomplished by addressing named controls on the UI and setting the required properties.

The application uses the Windows Phone 7.1 SDK APIs without any additional abstractions.

The Contents of the PetrolTracker.CodeBehind Project

The PetrolTracker.CodeBehind project organizes the source code and other resources into folders. The following table outlines what is contained in each folder.

Project folder

Description

Root

In the root folder of the project, you'll find the App.xaml file that every Windows Phone application project must include. This defines some of the startup behavior of the application. The root folder also contains some image files that all Windows Phone applications must include.

Properties

In this folder, you'll find two manifest files and the AssemblyInfo.cs file. The WMAppManifest.xml file describes the capabilities of the application and specifies the initial page to display when the user launches the application.

Data

This folder contains the static DataStore class that retrieves data from isolated storage.

Resources

This folder contains various image files that the application uses.

Toolkit.Content

This folder contains images used on the application bar.

View

This folder contains the views that define the pages in the application.

Accessing the Model

The model classes, which represent the domain entities used in the application, can be accessed through the static DataStore class. The illustration below shows the key model classes and the relationships between them.

Follow link to expand image

The static DataStore class contains a static property named Fleet, of type Fleet. This can be used to access the properties, events, and methods of the Fleet class. The Fleet class contains a property named CurrentVehicle, of type Vehicle, which can be used to access the properties and event of the Vehicle class. The Fleet class also contains a collection named Vehicles, which is of type IList<Vehicle>. This collection contains the three sample vehicles that are used by the application. The Vehicle class contains an ObservableCollection of type Fillup, named Fillups, which can be used to access the properties and methods of the FillUp class.

Application Launching Event

The Windows Phone execution model governs the lifecycle of applications running on a Windows Phone, from when the application is launched until it is terminated. For more information about the Windows Phone execution model, see Execution Model Overview for Windows Phone.

The following code example shows the Application_Launching event handler, which is raised when a new application instance is launched by the user.

void Application_Launching(object sender, LaunchingEventArgs e)
{
  …
  if (!IsolatedStorageSettings.ApplicationSettings.Contains(Constants.FleetKey))
  {
    var dataGenerator = new DataGenerator();
    var fleet = dataGenerator.CreateFleet();
    fleet.SetCurrentVehicle(fleet.Vehicles[0].VehicleId);
    IsolatedStorageSettings.ApplicationSettings.Add(Constants.FleetKey, fleet);
    IsolatedStorageSettings.ApplicationSettings.Save();
  }
}

The Application_Launching event handler examines the application settings, and if it does not contain the Fleet constant, a new instance of the DataGenerator class is created and the CreateFleet method is called on this class. The DataGenerator class creates a fleet of three sample vehicles and copies each vehicle's data to isolated storage. The current vehicle is set to the first item in the Vehicles collection, and then the IsoaltedStorageSettings dictionary is updated.

To meet the Windows Phone certification requirements, applications must complete application lifecycle events within 10 seconds.

The User Interface

The PetrolTracker code-behind implementation follows the UI design guidance published in the User Experience Design Guidelines for Windows Phone.

The User Experience Design Guidelines for Windows Phone describes best practices for designing the UI of a Windows Phone application.

The UI consists of three views, with each view containing resources and controls.

  • AddFillUp is used to add fill-up data for a vehicle.
  • Settings is used to select a vehicle for which to display fill-up data.
  • VehicleHistory displays the fill-up data for a selected vehicle.

The VehicleHistory view is displayed when the application first starts. The following code example shows how the application is configured to display the VehicleHistory view to the user.

<Tasks>
  <DefaultTask Name="_default" NavigationPage="/View/VehicleHistory.xaml" />
</Tasks>

The WMAppManifest.xml file sets the NavigationPage property of the DefaultTask element to the VehicleHistory view. This view is the first page that you will be shown.

VehicleHistory

The VehicleHistory view uses a Pivot containing two PivotItems, to display data. The first PivotItem contains two Images, while the second PivotItem contains a ListBox of the fill-up data. The following code example shows how the PivotItems are defined.

<controls:Pivot x:Name="vehicleHistoryPivot" … >
  <controls:PivotItem Header="charts" Margin="0">
    <Grid>
      …
      <Image x:Name="mpgHistoryChart"
        Source="/PetrolTracker.CodeBehind;component/Resources/Images/MPG.png"  
        Visibility="Collapsed" />
      <Image x:Name="costHistoryChart" 
        Source="/PetrolTracker.CodeBehind;component/Resources/Images/Cost.png" 
        Visibility="Collapsed" />
      <Grid Grid.Row="1" Margin="12,0" >
        …
        <RadioButton x:Name="btnMpg" … >
          <i:Interaction.Triggers>
            <i:EventTrigger EventName="Click">
              <ec:GoToStateAction StateName="MpgState"/>
            </i:EventTrigger>
          </i:Interaction.Triggers>
        </RadioButton>
        <RadioButton x:Name="btnCost" … >
          <i:Interaction.Triggers>
            <i:EventTrigger EventName="Click">
              <ec:GoToStateAction StateName="CostState"/>
            </i:EventTrigger>
          </i:Interaction.Triggers>
        </RadioButton>
      </Grid>
    </Grid>
  </controls:PivotItem>

  <controls:PivotItem Header="fill-ups">
    <Grid Margin="6,10,0,0">
      …  
      <ListBox 
        Grid.Row="1"
        ItemContainerStyle="{StaticResource ListBoxStyle}"
        ItemsSource="{Binding Source={StaticResource fillUpsCvs}}">
        <ListBox.ItemTemplate>
          <DataTemplate>
            <StackPanel Orientation="Horizontal">
              <TextBlock … Text="{Binding Path=DatePurchased, …}" … />
              <TextBlock … Text="{Binding Path=QuantityUnitsPurchased}" />
              <TextBlock … Text="{Binding Path=MilesDriven}" />
              <TextBlock … Text="{Binding Path=PricePerUnitPurchased, … }" />
              <TextBlock … Text="{Binding Path=Mpg, … }" />
            </StackPanel>
          </DataTemplate>
        </ListBox.ItemTemplate>
      </ListBox>
    </Grid>
  </controls:PivotItem>
</controls:Pivot>

The first PivotItem, containing the Images, also contains two re-styled RadioButtons. Each RadioButton uses an EventTrigger to set a VisualState when the RadioButton is tapped. Each image presents a sample chart of a different filtered view of the raw data. The first sample chart filters data based upon MPG, and the second sample chart filters data based upon cost. The data source for the ListBox is a CollectionViewSource named fillupCvs. The following code example shows how the CollectionViewSource is defined as a page-level Resource.

<phone:PhoneApplicationPage 
  … >
  <phone:PhoneApplicationPage.Resources>
    <CollectionViewSource x:Key="fillUpsCvs" />
     …
  </phone:PhoneApplicationPage.Resources>
  …
</phone:PhoneApplicationPage>

Notice that the CollectionViewSource does not have its Source property set, and so does not initially contain any data. The following code example shows how it is populated with data.

void RefreshFillUps() 
{
  var cvs = (CollectionViewSource) this.Resources["fillUpsCvs"];
  cvs.Source = DataStore.Fleet.CurrentVehicle.FillUps.Where(
    f => f.QuantityUnitsPurchased > 0).OrderByDescending(f => 
    f.DatePurchased).ThenByDescending(f => f.OdometerReading).ToList();
}

The RefreshFillUps method retrieves the fillUpCVs CollectionViewSource defined in the XAML and sets its Source property to a List containing the fill-up data for the current vehicle.

The VehicleHistory view also contains two ApplicationBarIconButtons on the application bar, which navigate to the Settings and AddFillUp views. The following code example shows how these ApplicationBarIconButtons are defined.

<phone:PhoneApplicationPage.ApplicationBar>
  <shell:ApplicationBar IsVisible="True">
    <shell:ApplicationBarIconButton 
      IconUri="/Resources/Images/appbar.feature.settings.rest.png" 
      Text="settings" Click="btnSettings_Click"/>
    <shell:ApplicationBarIconButton 
      IconUri="/Resources/Images/appbar.add.rest.png" 
      Text="add fill-up" Click="btnAddFillUp_Click"/>
  </shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>

Each ApplicationBarButton has an event handler attached to the Click event. The following code example shows how the event handlers navigate to the appropriate view.

void btnSettings_Click(object sender, EventArgs e) 
{
  this.NavigationService.Navigate(new Uri("/View/Settings.xaml", 
    UriKind.Relative));
}

void btnAddFillUp_Click(object sender, EventArgs e) 
{
  this.NavigationService.Navigate(new Uri("/View/AddFillUp.xaml",  
    UriKind.Relative));
}

Each event handler uses the Navigate method from the NavigationService API class to navigate to the appropriate view.

AddFillUp

The AddFillUp view uses a DatePicker and three TextBox instances to accept vehicle fill-up data. The following code example shows how these controls are defined.

<toolkit:DatePicker x:Name="dpPurchaseDate" 
  ValueChanged="dpPurchaseDate_ValueChanged" … />
<TextBox … Name="txtOdometer" InputScope="Digits" 
  TextChanged="AllTextBox_TextChanged" … />
<TextBox … Name="txtGallons" InputScope="Digits" 
  TextChanged="AllTextBox_TextChanged" … />
<TextBox … Name="txtPricePerGallon" InputScope="Digits" 
  TextChanged="AllTextBox_TextChanged" … />

Each control has the Name property set and has an event handler registered against a changed event.

The AddFillUp view also contains a Save button on the application bar, which saves the entered data. The following code example shows how the Save button is defined.

<phone:PhoneApplicationPage.ApplicationBar>
  <shell:ApplicationBar IsVisible="True">
    <shell:ApplicationBarIconButton
      IconUri="/Resources/Images/appbar.save.rest.png" Text="save" 
      Click="btnSave_Click"/>
  </shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>

The Save button is an ApplicationBarIconButton, which has an event handler attached to the Click event. The following code example shows the event handler for the click event.

void btnSave_Click(object sender, EventArgs e) 
{
  var errors = new List<string>();
  int odometerReading;
  double pricePerUnitPurchased;
  double quantityUnitsPurchased;

  // sanity check for nulls, should not happen.
  if(!this.dpPurchaseDate.Value.HasValue) 
  {
    errors.Add("Purchase date is required.");
  }

  …

  if(errors.Count > 0) 
  {
    MessageBox.Show(string.Join(Environment.NewLine, errors.ToArray()), 
      "Invalid Input", MessageBoxButton.OK);
    return;
  }

  // create and populate a new fill up object
  // Note: the dpPurchaseDate.Value has already been checked for null condition
  var fillUp = new FillUp 
  {
    DatePurchased = this.dpPurchaseDate.Value.Value,
    MilesDriven = odometerReading -  
      DataStore.Fleet.CurrentVehicle.CurrentOdometer,
    OdometerReading = odometerReading,
    PricePerUnitPurchased = pricePerUnitPurchased,
    QuantityUnitsPurchased = quantityUnitsPurchased
  };

  // validate new fill up object
  // fillUp.Validate returns a collection of strings for any errors.
  errors = new List<string>(
    fillUp.Validate(DataStore.Fleet.CurrentVehicle.CurrentOdometer));

  if(errors.Count > 0) 
  {
    MessageBox.Show(string.Join(Environment.NewLine, errors.ToArray()), 
      "Invalid Input", MessageBoxButton.OK);
    return;
  }

  // if no errors then add to fill ups collection and persist the data to 
  // application storage
  DataStore.Fleet.CurrentVehicle.FillUps.Add(fillUp);
  IsolatedStorageSettings.ApplicationSettings[Constants.FleetKey] = 
    DataStore.Fleet;
  IsolatedStorageSettings.ApplicationSettings.Save();

  this.NavigationService.GoBack();
}

This method first checks that the user has entered the required data, and if they haven't, displays error messages in a MessageBox. The entered data is then stored in a new instance of a FillUp object, and then validated with the Validate method on the FillUp object. If validation fails, a MessageBox is displayed informing the user of this. If validation passes, the FillUp object is added to the FillUps collection of the current vehicle, referenced through the static DataStore class. The data then persists to isolated storage, followed by navigation back to the previous page.

Settings

The Settings view uses a ListBox named FleetVehicles to display the three sample vehicles that the user can select from. The following code example shows the ListBox and the DataTemplate used by the view.

<ListBox Grid.Row="1" x:Name="FleetVehicles" Margin="24,0">
  …
  <ListBox.ItemTemplate>
    <DataTemplate>
      <Grid Margin="0,0,0,17" Width="432" Height="90">
        …
        <Image Source="{Binding Path=Picture}" … />
        <TextBlock Text="{Binding Path=VehicleName}" … />
        <TextBlock Text="{Binding Path=AverageMpg, …" … />
        <TextBlock Text="{Binding Path=CurrentOdometer, …" … />
      </Grid>
    </DataTemplate>
  </ListBox.ItemTemplate>
</ListBox>

The DataTemplate contains an Image and TextBlocks which bind to properties contained in the Vehicle model class. The following code example shows the constructor of the Settings class.

public Settings()
{
  InitializeComponent();
  this.FleetVehicles.ItemsSource = DataStore.Fleet.Vehicles;
  this.FleetVehicles.SelectedItem = DataStore.Fleet.CurrentVehicle;
  this.FleetVehicles.SelectionChanged += FleetVehicles_SelectionChanged;
}

The Settings constructor sets the ItemsSource property of the ListBox to the Vehicles collection of the static Fleet object, in the static DataStore class. It then sets the SelectedItem of the ListBox before registering an event handler to the SelectionChanged event of the ListBox. The following code example shows this event handler.

void FleetVehicles_SelectionChanged(object sender, SelectionChangedEventArgs e) 
{
  if (e.AddedItems != null && e.AddedItems[0] != null) 
  {
    DataStore.Fleet.SetCurrentVehicle(((Vehicle) e.AddedItems[0]).VehicleId);
    this.NavigationService.GoBack();
  }
}

The SelectionChanged event handler simply sets the selected item to the vehicle the user has tapped, before returning to the previous page.

Saving and Restoring Page State

When the user navigates away from your application, it goes dormant. If there is not enough memory available, your application may be tombstoned. If the user returns to your application, you will need to restore your application from the appropriate state.

Applications must restore their state if the user returns to the application after taking a call or using another application.

It's the application's responsibility to determine what state data it needs to save if it is to be able to restore the application to the same state when the application is reactivated.

When an application first navigates to a page, it instantiates the page, calls its constructor, and then calls its OnNavigatedTo method override, if present. Just before it navigates to a different page, exits, or deactivates, it calls the OnNavigatedFrom method. Page instances are reused only when the user navigates back to a previously visited page that is present in the backstack, and the application has not been deactivated since that visit. In this case, the application calls the OnNavigatedTo and OnNavigatedFrom methods as before, but without calling the constructor first. For more information about the application lifecycle, including the sequence of lifecycle events, see Execution Model Overview for Windows Phone.

In PetrolTracker, each view is responsible for persisting and reloading its own state. Therefore, when a page is navigated away from, the state of the UI for the page is saved in a state dictionary, and when a page is navigated to, the UI's state is restored to the page. When returning from dormancy, all object instances will still be in memory in the exact state they were in prior to dormancy, so the application does not need to perform any additional tasks. However, when returning from a tombstoned state, the code-behind will restore the UI state of the view so that it is back in the state it was in when it was originally deactivated.

The page-level OnNavigatedTo and OnNavigatedFrom event handlers are used to handle the persistence and restoration of UI state. State is saved to the page-level State property, which is a dictionary that stores key/value pairs where the values are primitive value types or serializable types. The following code example shows how the AddFillUp view saves and restores its state.

const string PurchaseDateKey = "PurchaseDateKey";
const string OdometerKey = "OdometerKey";
const string GallonsKey = "GallonsKey";
const string PricePerGallonKey = "PricePerGallonKey";
const string HasChangesKey = "HasChangesKey";
bool hasChanges;
bool isNewInstance;

protected override void OnNavigatedTo(NavigationEventArgs e)
{
  base.OnNavigatedTo(e);

  // if coming back from dormant state, ignore this block of code
  if(isNewInstance) 
  {
    this.DataContext = DataStore.Fleet.CurrentVehicle;

    // restore view controls if returning from tombstone and values are saved
    if(PhoneApplicationService.Current.StartupMode == StartupMode.Activate && 
      this.State.Count > 0)
    {
      var pageStateHelper = new PageStateHelper(this.State);
      this.dpPurchaseDate.Value = pageStateHelper.GetValue<DateTime?>(
        PurchaseDateKey, DateTime.Now);
      this.txtOdometer.Text = pageStateHelper.GetValue(OdometerKey, string.Empty);
      this.txtGallons.Text = pageStateHelper.GetValue(GallonsKey, string.Empty);
      this.txtPricePerGallon.Text = pageStateHelper.GetValue(PricePerGallonKey, 
        string.Empty);
      hasChanges = pageStateHelper.GetValue(HasChangesKey, false);
      CalculateReadOnlyFields();
    }
  }
  isNewInstance = false;
}

protected override void OnNavigatedFrom(NavigationEventArgs e) 
{
  base.OnNavigatedFrom(e);

  // if not navigating back, save page state.
  if(e.NavigationMode == NavigationMode.Back) 
  {
    return;
  }
  this.State[PurchaseDateKey] = this.dpPurchaseDate.Value;
  this.State[OdometerKey] = this.txtOdometer.Text;
  this.State[GallonsKey] = this.txtGallons.Text;
  this.State[PricePerGallonKey] = this.txtPricePerGallon.Text;
  this.State[HasChangesKey] = hasChanges;
}

The OnNavigatedTo method is called by the page's Navigated event when the page is navigated to. It examines the isNewInstance flag, which indicates whether the page is a new instance. If it's a new page instance, the DataContext of the AddFillUp view is set to the current vehicle. The method then checks if the application is returning from a tombstoned state and if there is any data saved in the page-level State dictionary. If both conditions are true, the method retrieves the data from the page-level State dictionary and the UI state is restored with the help of the PageStateHelper class. In addition, this method will also get called when navigating page to page, in which case the UI state does not need to be updated from the page-level State dictionary. Finally, the isNewInstance flag is set to false to indicate that the application is no longer a new instance. The OnNavigatedFrom method is called by the page's Navigated event when the page is navigated away from. Provided that the user is not navigating backwards, the user-entered fill-up data is stored in the page-level State dictionary.

The PageStateHelper class is used to retrieve and cast objects saved in page state, and has a method that loads any state that the page needs when it's navigated to. The following code example shows the complete PageStateHelper class.

public class PageStateHelper 
{    
  readonly IDictionary<string, object> state;

  /// <summary>
  /// Initializes a new instance of the <see cref="PageStateHelper"/> class.
  /// </summary>
  /// <param name="state">The page state dictionary.</param>
  public PageStateHelper(IDictionary<string, object> state)
  {
    if (state == null) throw new ArgumentNullException("state");
    this.state = state;
  }

  /// <summary>
  /// Gets the value.
  /// </summary>
  /// <typeparam name="T"></typeparam>
  /// <param name="key">The key.</param>
  /// <param name="defaultValue">The default value.</param>
  /// <returns>Saved instance of T or the defaultValue</returns>
  public T GetValue<T>(string key, T defaultValue) 
  {
    object value;
    if (state.TryGetValue(key, out value)) 
    {
      if (value is T) 
      {
        return (T) value;
      }
    }
    return defaultValue;
  }
}

The constructor takes a page-level state dictionary as a parameter, and stores it. The GetValue method is used to retrieve a value from the page state dictionary, based upon its key.

This implementation provides basic page-state support, but does not store every aspect of the page state. Specifically, this implementation does not store a value that indicates which text box has focus, nor does it store the cursor position and selection state of the focused text box. The importance of saving this state information depends on the application, but for implementation suggestions, see How to: Preserve and Restore Page State for Windows Phone.

Summary

The PetrolTracker code-behind application enables a user to track the petrol consumption of three sample cars. The application follows the pattern of UI gestures being forwarded to the code-behind as events. When an event is raised, its corresponding event handler in the code-behind is executed. Each handler performs the required operation and then sets the state of the page. Setting the state of the page is accomplished by addressing named controls on the UI and setting the required properties.

The main problem with this implementation is that the UI controls and business logic are tightly coupled. This tight coupling can result in all sorts of maintenance issues which could translate into poor customer satisfaction with the delivered application. Therefore, an approach is required that completely separates the UI from the business logic. Such an approach will make applications easier to test, maintain, and evolve.

Next Topic | Previous Topic | Home

Last built: February 10, 2012