Mobile Corner

Retrieving, Storing and Displaying Data in Windows Phone Apps

Nick Randolph connects a Windows Phone application to the cloud to save and retrieve data, then uses a local SQLite database to cache data for offline use.

Windows Phone applications have different needs when it comes to accessing, storing and synchronizing data. Most consumer applications typically consume data -- displaying the contents of a news feed, for example -- and can get away with minimal or even no caching of data. Enterprise applications, on the other hand, typically have to be architected for the content to be available in offline or occasionally-connected scenarios. In this article we'll start with a design of a simple application, connect it to real data services, and then add synchronization logic to add the ability to work offline. We'll be using the Windows Azure Mobile Services, but the basic principles of data synchronization can be applied to other technologies.

We'll start of by building out the design of our application, which is a simple, two-page defect-capturing application. The first page contains a list of defects that have been logged; the second page will enter a new defect. First create a new Visual Studio project, called Defects, based on the Windows Phone App project template. We'll immediately add a second page, AddDefectPage.xaml, before switching over to Blend to design the layout of both pages.

To assist with page design, we'll create two sets of design time data using the Data window in Blend. Figure 1 illustrates the AddDefectDataSource, made up of a collection of DefectTypes and a NewDefect object, and the MainDataSource, which is a collection of Defects.

Figure 1. Design-Time Data

When creating design time data, uncheck the "Enable sample data when application is running" option, as we'll be wiring up runtime data and don't want the design time data to get in the way. To create the layout of the main page, drag the Defects collection from the Data window into the main area of the page. Right-click on the newly created ListBox and select Reset Layout -> All, which will expand the ListBox to consume the available space. Right-click on the ListBox again and select Edit Additional Templates -> Item Template -> Edit Current. Now you can adjust the item template to improve the layout of each item.

Figure 2 illustrates how each of the defects is displayed, using the following item template:

[Click on image for larger view.] Figure 2. How the Defects Are Displayed

<DataTemplate x:Key="DefectsItemTemplate"> <Grid Margin="12,0,0,24"> <Grid.ColumnDefinitions> <ColumnDefinition Width="12"/> <ColumnDefinition Width="Auto"/> <ColumnDefinition/> </Grid.ColumnDefinitions> <Border Background="{StaticResource PhoneAccentBrush}"/> <Grid Grid.Column="1" Width="100" Height="100" VerticalAlignment="Top"> <Border BorderBrush="Black" BorderThickness="1" Background="{StaticResource PhoneContrastBackgroundBrush}" Opacity="0.1"/> <Image Source="{Binding Photo}"/> </Grid> <StackPanel Orientation="Vertical" Grid.Column="2" Margin="0,-6"> <TextBlock Text="{Binding Title}" Style="{StaticResource PhoneTextNormalStyle}" TextWrapping="Wrap" MaxHeight="82"/> <TextBlock Text="{Binding DefectType}" Style="{StaticResource PhoneTextSubtleStyle}"/> </StackPanel> </Grid> </DataTemplate>

Currently, there's a Border on the left side of each defect, which is filled using the accent color on the device (in this case, red). This is supposed to represent the severity of the defect: Red when Severity > 5; otherwise Green. To do this we're going to data bind the background color of the Border to the Severity property on the Defect. As the Severity is a number and the Background is a brush, we'll need to use the SeverityColorConverter to translate between the types:

public class SeverityColorConverter : IValueConverter { public SolidColorBrush HighPriorityColor { get; set; } public SolidColorBrush DefaultAndLowPriorityColor { get; set; } public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { try { var data = value + ""; var dataValue = double.Parse(data); if (dataValue > 5) { return HighPriorityColor; } return DefaultAndLowPriorityColor; } catch (Exception ex) { return DefaultAndLowPriorityColor; } } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { return value; } }

An instance of the SeverityColorConverter is added to the Resources collection on the page, with attributes DefaultAndLowPriorityColor and HighPriorityColor set in XAML:

<local:SeverityColorConverter x:Key="SeverityColorConverter" DefaultAndLowPriorityColor="Green" HighPriorityColor="Red"/>

The Background attribute of the Border within the item template can be updated to use the SeverityColorConverter instance as a converter:

<Border Background="{Binding Severity, Converter={StaticResource SeverityColorConverter}}"/>

The last thing to do on the MainPage is add two application bar buttons, Add and Sync, which will be wired up shortly.

Next, we'll update the AddDefectPage to look like Figure 3.

[Click on image for larger view.] Figure 3. The Add Defect Page

Here's the complete XAML mark up for this page: <phone:PhoneApplicationPage 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:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc= "http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:toolkit="clr-namespace:Microsoft.Phone.Controls; assembly=Microsoft.Phone.Controls.Toolkit" x:Class="Defects.AddDefectPage" FontFamily="{StaticResource PhoneFontFamilyNormal}" FontSize="{StaticResource PhoneFontSizeNormal}" Foreground="{StaticResource PhoneForegroundBrush}" mc:Ignorable="d" shell:SystemTray.IsVisible="True"> <phone:PhoneApplicationPage.Resources> <DataTemplate x:Key="DefectTypesItemTemplate"> <TextBlock Text="{Binding Name}" /> </DataTemplate> </phone:PhoneApplicationPage.Resources> <phone:PhoneApplicationPage.ApplicationBar> <shell:ApplicationBar> <shell:ApplicationBarIconButton IconUri="/Assets/AppBar/feature.camera.png" Text="photo" /> <shell:ApplicationBarIconButton IconUri="/Assets/AppBar/save.png" Text="save" /> </shell:ApplicationBar> </phone:PhoneApplicationPage.ApplicationBar> <Grid x:Name="LayoutRoot" Background="Transparent" d:DataContext= "{Binding Source={StaticResource AddDefectDataSource}}"> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <StackPanel Grid.Row="0" Margin="12,17,0,28"> <TextBlock Style="{StaticResource PhoneTextNormalStyle}" Text="DEFECTS" /> <TextBlock Text="new defect" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}" /> </StackPanel> <StackPanel x:Name="ContentPanel" Grid.Row="1" Margin="12,-12,12,0"> <Grid Margin="{StaticResource PhoneMargin}" VerticalAlignment="Top" HorizontalAlignment="Left" Width="200" Height="200"> <Border BorderBrush="Black" BorderThickness="1" Background= "{StaticResource PhoneContrastBackgroundBrush}" Opacity="0.1" /> <Image Source="{Binding NewDefect.Photo}" /> </Grid> <TextBlock Text="Title" Style="{StaticResource PhoneTextSubtleStyle}" /> <TextBox Height="72" TextWrapping="Wrap" Text="{Binding NewDefect.Title, Mode=TwoWay}" /> <toolkit:ListPicker Header="Defect Type" ItemTemplate="{StaticResource DefectTypesItemTemplate}" ItemsSource="{Binding DefectTypes}" FullModeItemTemplate= "{StaticResource DefectTypesItemTemplate}" /> </StackPanel> </Grid> </phone:PhoneApplicationPage>

We can now run the application, without data, and view the basic layout for both pages. In order to see the AddDefectPage, we'll add in an event handler for the Add application bar button:

private void AddDefectClick(object sender, EventArgs e) { NavigationService.Navigate( new Uri("/AddDefectPage.xaml", UriKind.Relative)); }

After designing the pages in the application, we can now progress to connecting up data. For this we'll start by defining the Defect and DefectType entities. As we'll be data binding to the Defect entity, it needs to implement the INotifyPropertyChanged interface:

public class Defect : INotifyPropertyChanged { public int Id { get; set; } public string UniqueId { get; set; } public string Title { get; set; } public string DefectType { get; set; } public int Severity { get; set; } public string PhotoUrl { get; set; } public Defect() { UniqueId = Guid.NewGuid().ToString(); PhotoUrl = string.Empty; Title = string.Empty; DefectType = string.Empty; } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChangedEventHandler handler = PropertyChanged; if (handler != null) handler( this, new PropertyChangedEventArgs(propertyName)); } } public class DefectType { public int Id { get; set; } public string Name { get; set; } public DateTime LastUpdated { get; set; } }

Each page in the application will be associated with a view model, which will hold the data to be displayed. Again, as the view models will be data bound, they also need to implement the INotifyPropertyChanged interface. Rather than implementing this on every view model, we'll create a BaseViewModel which provides this implementation:

public class BaseViewModel : INotifyPropertyChanged { public Repository Repository { get; set; } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChangedEventHandler handler = PropertyChanged; if (handler != null) handler( this, new PropertyChangedEventArgs(propertyName)); } }

The MainViewModel and AddDefectViewModel inherit from BaseViewModel and expose properties that align with the design time data created earlier:

public class MainViewModel : BaseViewModel { private readonly ObservableCollection<Defect> defects = new ObservableCollection<Defect>(); public ObservableCollection<Defect> Defects { get { return defects; } } } public class AddDefectViewModel : BaseViewModel { public AddDefectViewModel() { NewDefect = new Defect(); } public Defect NewDefect { get; set; } private readonly ObservableCollection<DefectType> defectTypes = new ObservableCollection<DefectType>(); public ObservableCollection<DefectType> DefectTypes { get { return defectTypes; } } }

The list of defect types for the application are going to be downloaded from Windows Azure Mobile Services (WAMS). Similarly, new defects will be saved to WAMS. As such, it makes sense for the logic to access WAMS to be kept together. We'll create an entity called Repository, which will be used to connect to WAMS. The Repository needs to be created when the application starts and passed into both view models (the Repository property has been pushed into the BaseViewModel):

public class Repository { public static MobileServiceClient MobileService = new MobileServiceClient( "https://<your service>.azure-mobile.net/", "<your service application key>"); }

To create the Repository, create instances of the view models and assign the Repository to them we have another entity, the ViewModelLocator:

public class ViewModelLocator { private Repository Repository { get; set; } public ViewModelLocator() { Repository=new Repository(); } public MainViewModel Main { get { return new MainViewModel {Repository = Repository}; } } public AddDefectViewModel AddDefect { get { return new AddDefectViewModel {Repository = Repository}; } } }

An instance of the ViewModelLocator can be created in XAML within the Resources dictionary of App.xaml file:

<Application.Resources> <SampleData:AddDefectDataSource x:Key="AddDefectDataSource" d:IsDataSource="True"/> <SampleData1:MainDataSource x:Key="MainDataSource" d:IsDataSource="True"/> <defects:ViewModelLocator x:Key="Locator" /> </Application.Resources>

Also in XAML, an instance of the corresponding view model can be data bound to the DataContext of each of the pages. We can add helper properties to make it easier to reference the view model within the code-behind file: