Understanding and Using OData – Creating a Windows Phone OData Client (4 of 4)
This article is part of a series:
- Understanding and Using OData (1 of 4)
- OData Publication (2 of 4)
- Analysing OData Feeds (3 of 4)
- Creating a Windows Phone 7 OData Client Application (4 of 4) – This article
Hopefully these articles will show you how to produce and publish an OData Feed, Create relationships between feeds from different sources, analyse OData and finally how to develop a custom OData client. This should give a really good appreciation of some of the possible uses for OData.
In this article, I will describe how to create a custom client application to consume the OData feed. Given the fact that I had recently signed up as a Windows Phone App Hub Developer, I thought a Windows Phone client application would be an interesting challenge.
I am using Visual Studio 2010, and I needed to install Windows Phone Developer Tools and the October Update.
I also downloaded the OData Client Library for Windows Phone 7 Series CTP and amCharts for Windows Phone 7. I unzipped both of these to scratch folders on my desktop.
Once I had done this I created a new ‘Data-bound Silverlight Application for Windows Phone’:
Once the application had been created, I copied System.Data.Services.Client.dll and AmCharts.Windows.QuickCharts.WP into my solution folder and added a references in my project:
It should be a simple case of adding a Service Reference to the OData service, but this does not appear to work in WP7 projects at the moment. Instead I followed these instructions to create a proxy class for the OData service. This involves running DataSvcUtil from %windir%\Microsoft.NET\Framework\v4.0.30128:
DataSvcutil.exe
/uri: https://odata.sqlazurelabs.com/OData.svc/v0.1/frq6joxf0e/DWP
/DWPPublicationModel /Version:2.0 /out:DWPPublicationServiceClient.cs
I included this proxy in my project, and can now access the OData service to retrieve the data. I decided to use the New Deal data described in the first article of this series [update hyperlink]:
I envisaged a simple application that simply displayed a list of providers, and when a provider was selected, the application shows a list of contract areas in which the provider operates, together with a graph of the statistics for that contract:
In order to support this I needed a fairly simple view model:
The next step was to populate the ProviderViewModel from the OData service via the DWPPublicationServiceClient proxy.
App.xaml from the scaffold “databound” application defines your application. The code behind (App.xaml.cs) is already wired up to create a MainViewModel.
MainViewModel contains an ItemViewModel, and we need to replace this with my custom ProviderViewModel:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Data.Services.Client;
using System.Linq;
using DWPPublicationModel; // namespace for the OData Service Proxy (generated by DataSvcUtil.exe).
namespace nikkh.NewDeal
{
public class MainViewModel : INotifyPropertyChanged
{
// Data context for OData service through proxy
private DWPPublications context;
// Private collection of newdeal entities (returned from the OData service, and defined in the proxy)
private DataServiceCollection<newdeal> _newdeals;
///// <summary>
///// Providers Property; An observable collection of ProviderViewModels (used for data binding in UI)
///// </summary>
///// <returns></returns>
public ObservableCollection<ProviderViewModel> Providers { get; private set; }
///// <summary>
///// MainViewModel Constructor; Build new top level view model
///// </summary>
///// <returns>MainViewModel</returns>
public MainViewModel()
{
// Initialise the ProviderViewModel collection
this.Providers = new ObservableCollection<ProviderViewModel>();
// initialise the OData service proxy
context = new DWPPublications(new Uri("https://ukgovodata.cloudapp.net/DWPPublicationService.svc/"));
// initialise the collection of newdeal entities to be retrieved via the Odata service proxy
_newdeals = new DataServiceCollection<newdeal>(context);
}
///// <summary>
///// GetNewDealStats; Method invoked by MainPage.xaml to initiate asynchronous call to OData service.
///// Processing is complete when the asynchronous call completes by the newDeals_LoadCompleted method
///// </summary>
///// <returns></returns>
public void GetNewDealStats(int numberOfNewDeals)
{
// clear the collection of newdeal entities to be retrieved via the Odata service proxy
_newdeals.Clear();
// register event handler to handle completion of asynchronous proxy request
_newdeals.LoadCompleted += new EventHandler<LoadCompletedEventArgs>(newdeals_LoadCompleted);
// load the newdeals from the proxy asynchronously
_newdeals.LoadAsync(context.newdeal.Take(numberOfNewDeals));
// set flag to signify that loading is in progress
this.IsDataLoaded = true;
}
///// <summary>
///// newdeals_LoadCompleted; Event to handle completion of asynchronous call to OData service
///// </summary>
///// <returns></returns>
void newdeals_LoadCompleted(object sender, LoadCompletedEventArgs e)
{
// Temporary collection to register whether the current provider already exists in the view model
// (This could be refactored to be more elegant, but I am an architect, so no need :-)).
Collection<string> providerList = new Collection<string>();
// For each New Deal Record retrieved from the OData service...
foreach ( newdeal n in _newdeals)
{
// Create a low-level provider performance record
ProviderPerformanceRecord ppr = new ProviderPerformanceRecord(n.Provider, n.Contract_Package_Area,
n.Starts_, n.Short_Job_Outcomes, n.Sustained_Job_Outcomes);
// If this provider has been encountered before
if (providerList.Contains(n.Provider))
{
// select the appropriate ProviderViewModel from the collection (there will only be one)
var item = from p in this.Providers where p.ProviderName == n.Provider select p;
// add the provider performance record to the ProviderViewModel
foreach (var i in item) i.PerformanceRecords.Add(ppr);
}
// If this provider has *not* been encountered before
else
{
// create a new ProviderViewModel
ProviderViewModel p = new ProviderViewModel(n.Provider);
// add the performance record to the new ProviderViewModel
p.PerformanceRecords.Add(ppr);
// add the ProviderViewModel to the View Model Collection
this.Providers.Add(p);
// add the provider to the temporary collection to signify it has been encountered before
providerList.Add(n.Provider);
}
} // next New Deal from the OData service
} // end of newDeals_LoadCompleted method
///// <summary>
///// public property; signifies whether the view model is loading or loaded
///// stops multiple asynchronous invocations.
///// </summary>
///// <returns>true if data is loaded, otherwise false</returns>
public bool IsDataLoaded
{
get;
private set;
}
///// <summary>
///// Implementation of the INotifyPropertyChanged interface (not yet implemented fully)
///// </summary>
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (null != handler)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
The MainViewModel depends on two other classes:
- ProviderViewModel
- ProviderPerformanceRecords
This is the ProviderViewModel class (which is effectively just a data structure for binding in the XAML:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace nikkh.NewDeal
{
public class ProviderViewModel : INotifyPropertyChanged
{
// private default constructor
private ProviderViewModel(){}
// public constructor
public ProviderViewModel(string provider)
{
// set member variable holding provider name
this._providerName = provider;
}
// member variable for provider name
private string _providerName;
// Public Property: ProviderName
public string ProviderName
{
get{return _providerName;}
set{
if (value != _providerName)
{
_providerName = value;
NotifyPropertyChanged("ProviderName");
}
}
}
// member variable for Provider Perfromance Records
private ObservableCollection<ProviderPerformanceRecord> _performanceRecords = new ObservableCollection<ProviderPerformanceRecord>();
// Public Property: Performance Records
public ObservableCollection<ProviderPerformanceRecord> PerformanceRecords
{
get{return _performanceRecords;}
set{
if (value != _performanceRecords)
{
_performanceRecords = value;
NotifyPropertyChanged("PerformanceRecords");
}
}
}
///// <summary>
///// Implementation of the INotifyPropertyChanged interface (not yet implemented fully)
///// </summary>
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (null != handler)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
The performance records class is a collection within the provider view model. Again this is really just a data structure for binding in the XAML, but this class also contains some redundant information (e.g. Provider Name) to make the binding easier:
using System.Collections.ObjectModel;
namespace nikkh.NewDeal
{
// Class to contain details of each Provider Performance Record
public class ProviderPerformanceRecord
{
// private default constructor
private ProviderPerformanceRecord() { }
// public constructor to build a performance record
public ProviderPerformanceRecord(string provider, string contactPackageArea,
decimal? starts, decimal? shortJobOutcomes, decimal? sustainedJobOutcomes)
{
_provider=provider;
_contractPackageArea=contactPackageArea;
_barItems.Add(new BarItem() { Name = "starts", Value = (double)starts });
_barItems.Add(new BarItem() { Name = "short", Value = (double)shortJobOutcomes });
_barItems.Add(new BarItem() { Name = "sustained", Value = (double)sustainedJobOutcomes });
}
// Public Property: Provider (holds Provider Name)
private string _provider;
public string Provider
{
get { return _provider; }
set { _provider = value; }
}
// Public Property: ContractPAckageArea (holds the area where the contract is held)
private string _contractPackageArea;
public string ContractPackageArea
{
get { return _contractPackageArea; }
set { _contractPackageArea = value; }
}
// Public Property: BarItems (holds points for Bar Chart)
private ObservableCollection<BarItem> _barItems = new ObservableCollection<BarItem>();
public ObservableCollection<BarItem> BarItems { get { return _barItems; } }
public class BarItem
{
public string Name { get; set; }
public double Value { get; set; }
}
}
}
The first page of the application (MainPage.xaml.cs) calls into ItemViewModel.GetData(). We need to replace this with a call to ProviderViewModel.GetNewDealStats(int ItemsToGet):
// Constructor
public MainPage()
{
InitializeComponent();
// Set the data context of the listbox control to the sample data
App.ViewModel.GetNewDealStats(30);
DataContext = App.ViewModel;
this.Loaded += new RoutedEventHandler(MainPage_Loaded);
}
When the application executes, App.Xaml creates a MainViewModel, which in turn creates a ProviderViewModel. This is populated with data from the OData service, and contains all the data necessary for this simple application.
The next step is to create the user interface. The template application already has two suitable pages:
- MainPage.xaml
- DetailsPage.xaml
Note that in MainPage.xaml.cs there is an event handler for a change of selection in the ListBox control:
// Handle selection changed on ListBox
private void MainListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// If selected index is -1 (no selection) do nothing
if (MainListBox.SelectedIndex == -1)
return;
// Navigate to the new page
NavigationService.Navigate(new Uri("/DetailsPage.xaml?selectedItem="
+ MainListBox.SelectedIndex, UriKind.Relative));
// Reset selected index to -1 (no selection)
MainListBox.SelectedIndex = -1;
}
This will handle the page transition between our two simple pages.
Firstly we need to change MainPage.xaml to understand a ProviderViewModel rather than the default ItemViewModel:
<!- Some Attributes Removed from PhoneApplicationPage element à
&lt;phone:PhoneApplicationPage
x:Class="nikkh.NewDeal.MainPage"
SupportedOrientations="Portrait" Orientation="Portrait"
shell:SystemTray.IsVisible="True">
<!-- LayoutRoot contains the root grid where all other page content is placed-->
<Grid x:Name="LayoutRoot" Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!--TitlePanel contains the name of the application and page title-->
<StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
<TextBlock x:Name="ApplicationTitle" Text="New Deal Success Rates"
Style="{StaticResource PhoneTextNormalStyle}" FontSize="32" />
<TextBlock x:Name="PageTitle" Text="Providers" Margin="9,-7,0,0"
Style="{StaticResource PhoneTextTitle1Style}"/>
</StackPanel>
<!--ContentPanel contains ListBox and ListBox ItemTemplate. Place additional content here-->
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<ListBox x:Name="MainListBox" Margin="0,0,-12,0" ItemsSource="{Binding Providers}"
SelectionChanged="MainListBox_SelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Margin="0,0,0,17" Width="432">
<TextBlock Text="{Binding ProviderName}"
TextWrapping="Wrap" Style="{StaticResource PhoneTextExtraLargeStyle}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Grid>
</phone:PhoneApplicationPage>
Note that I have changed the ApplicationTitle and PageTitle elements and font sizes. I have also changed the ItemsSource for the MainListBox to Providers. This is the Providers property of the MainViewModel that is an ObservableCollection of ProviderViewModel.
Finally, I have populated the TextBlock within the MainListBox with the ProviderName property. This property is resolved to ProviderViewModel.ProviderName for each item in MainViewModel.Providers.
Then we need to change DetailsPage.xaml to display the lower level performance records for the selected provider, and draw a simple graph using the performance data in each record:
<!-- Some Attributes Removed from PhoneApplicationPage element -->
<phone:PhoneApplicationPage
x:Class="nikkh.NewDeal.DetailsPage"
SupportedOrientations="Portrait" Orientation="Portrait"
shell:SystemTray.IsVisible="True">
<!-- LayoutRoot contains the root grid where all other page content is placed-->
<Grid x:Name="LayoutRoot" Background="Transparent" >
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!--TitlePanel contains the name of the application and page title-->
<StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
<TextBlock x:Name="PageTitle" Text="New Deal Success Rates"
Style="{StaticResource PhoneTextNormalStyle}" FontSize="32" />
<TextBlock x:Name="ListTitle" Text="{Binding ProviderName}"
Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}" />
</StackPanel>
<!--ContentPanel contains details text. Place additional content here-->
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0" >
<ListBox x:Name="list1" ItemsSource="{Binding \PerformanceRecords}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">
<TextBlock x:Name="tb1" Text="{Binding ContractPackageArea}"
TextWrapping="Wrap" Style="{StaticResource PhoneTextNormalStyle}" FontSize="30" />
<Grid x:Name="ContentGrid" Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<amq:SerialChart x:Name="chart1" DataSource="{Binding BarItems}" CategoryValueMemberPath="Name"
AxisForeground="White"
PlotAreaBackground="Black"
GridStroke="DarkGray" MinHeight="400">
<amq:SerialChart.Graphs>
<amq:ColumnGraph ValueMemberPath="Value"
Title="" Brush="#8000FF00"
ColumnWidthAllocation="0.4" />
</amq:SerialChart.Graphs>
</amq:SerialChart>
</Grid>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Grid>
</phone:PhoneApplicationPage>
Note that I have again changed the Page title, and bound the ListTitle to MainViewModel.Providers(n).ProviderName.
This time the list is bound to the MainViewModel.Providers.PerformanceRecords, which means there will be one entry for each performance record for the selected provider. This provides access to the ContractPackageArea property.
The chart is drawn using the AM Charts Library, which we downloaded and referenced at the beginning of this article. This library will draw a bar chart from a simple collection of points, that are composes of a name and a value. In order to draw the chart, the chart control is bound to the MainViewModel.Providers.PerformanceRecords.BarItems collection.
And that is all that we require to in order to run the application. (Note that there is little or no exception handing in the samples, to try to keep the code snippets in this article as concise as possible).
If we run the application in the Windows Phone 7 Emulator:
Then we get the following screens:
If you would like to deploy your application to you actual WP7 device, you will need to unlock it first. I did this by registering on the Windows Phone App Hub.
So now, I can make a change to the CSV file containing the New Deal Data, run my SSIS package, refresh the page on the phone and see the change reflected in the graph!
This is the last article in my series relating to creating and consuming OData feeds. I hope you have found it useful?
Written by Nick Hill