Partager via


Understanding and Using OData – Creating a Windows Phone OData Client (4 of 4)

This article is part of a series:

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’:

image_4_5EBCDB2A[1]

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:

image_6_5EBCDB2A

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]:

 

image_8_5EBCDB2A

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:

image_10_5EBCDB2A

In order to support this I needed a fairly simple view model:

image_12_5EBCDB2A

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 à
&amp;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:

image_14_49CB58B7

Then we get the following screens:

image_16_49CB58B7

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