Using WCF Data Services with WinRT

In this post I will walk through the steps of creating an OData / WCF Data Service and consume it from a Windows 8 Metro Style XAML based application.  I will use the following technology stack:

  • SQL Server
  • Entity Framework Code First
  • WCF Data Service
  • WinRT / Metro Style Application

To get started you have to have installed Visual Studio 11 RC on a Windows 8 PC.  You will also need to install SQL Server.  I am using 2012 version, but you can use any version you like that EF Code First supports.

Start by creating a new solution with a Metro Style application.  Used Blank Application template, but you can use whichever template you like.  Then add a new project for Windows Class Library using 4.5 version of the framework.  In this project we will house entity framework entity classes and DbContext class.  One note.  There seems to be an issue in RC where if you split DbContext and entities into two assemblies, you might get an error later in WCF Data Service.  Then add another project for web application using Empty Web Application project template.  Later will add a new item using WCF Data Service template.  Let’s get started.

First of all, I am going to create an entity and a model to use with Entity Framework.  I am going to keep it simple, demonstrating basic concepts.  I am going to have an entity called Session and context to house it in.

namespace WinRT.Data
{
    public class Session
    {
        public int SessionID { get; set; }
        public string Title { get; set; }
        public string Description { get; set; }
        public string Speaker { get; set; }
        public DateTime When { get; set; }
    }
}

Context will contain a DbSet of Session bas well as configure the Session object for database storage.  I could have use separate configuration class, but this is single table context, so I am not going to bother with this.

namespace WinRT.Data
{
    public class Context : DbContext
    {
        public Context() :
            base("Name=VSLive")
        {
        }
        public DbSet<Session> Sessions { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            modelBuilder.Entity<Session>().Property(p => p.Title).HasMaxLength(100).IsRequired();
            modelBuilder.Entity<Session>().Property(p => p.Speaker).HasMaxLength(50).IsRequired();
            modelBuilder.Entity<Session>().Property(p => p.Description).IsRequired();
            modelBuilder.Entity<Session>().Property(p => p.When).IsRequired();
        }
    }
}

Now in my web project I am going to add new item of type WCF Data Service

image

Next I need to configure my service.  I need to write a bit extra code to expose underlying Object Context behind DbContext because I need to update, not only query the data and I need to satisfy IUpdatable interface requirements.  I also need to turn off proxy creation to avoid serialization errors.

namespace WinRTWcfService
{
    [System.ServiceModel.ServiceBehavior(IncludeExceptionDetailInFaults = true)]   
    public class VSLiveWcfDataService : DataService<ObjectContext>
    {
        public static void InitializeService(DataServiceConfiguration config)
        {
            config.SetEntitySetAccessRule("*", EntitySetRights.All);
            config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
            config.UseVerboseErrors = true;
        }

        protected override ObjectContext CreateDataSource()
        {
            var ctx = new Context();
            var objectContext = ((IObjectContextAdapter)ctx).ObjectContext;
            objectContext.ContextOptions.ProxyCreationEnabled = false;
            objectContext.ContextOptions.LazyLoadingEnabled = false;
            return objectContext;
        }

        protected override void HandleException(HandleExceptionArgs args)
        {
            base.HandleException(args);
        }
    }
}

I would like to point out a few things.  One, is that I override HandleException method.  I did that in order to troubleshoot problems I encountered.  I also overrode CreateDataSource method in order to expose ObjectContext and turn off lazy loading and proxy creation.  I also setup access rights in InitializeSerice as well as turned on verbose errors.  I also setup attribute to inlcude exception details in faults, but you never want to deploy like that, so you can surround the attribute by #IF DEBUG / #ENDIF directives.

Now, I need to setup connection string in my web.config and I am ready to test my service.  Because of many changes to WCF in later versions, it now assumes a lot of configuration options.  To test simple open the browser and navigate to (in my example):  http://localhost/WinRTWcfService/VSLiveWcfDataService.svc/Sessions

You should see my data that I populated via initializer.  If you are interested in specifics, download entire source code using the download link at the end of the post.

Next I am ready to update my WinRT Metro application project.  Just right click on service references and either use Discover services in the solution or just type in service URL you used for testing withou Sessions portion.  You may need to add $metadata to the end of URL.  It seems to work without it for me a few times, but failed a few as well.  Weird, but I am attributing this to RC.  I am sure this will be fixed in RTM.

image

 

Now, I can write my View Model.  One thing to notice is that I have to use Dispatcher to handle service callbacks, so somehow I need to pass dispatcher into it.  I am taking a shortcut in my app.xaml.cs:

        protected override void OnLaunched(LaunchActivatedEventArgs args)
        {

            var vm = new WcfDataServicesViewModel();
            
            Window.Current.Content = new MainPage() { DataContext = vm };
            vm.LoadData(Window.Current.Dispatcher);
            Window.Current.Activate();
        }

Also my model will need the URL for the deployed service.  I usually use #IF DEBUG for that:

#if DEBUG
        private const string _serviceUri = @"http://localhost/WinRTWcfService/VSLiveWcfDataService.svc/";
#else
        private const string _serviceUri = @"http://www.realsite.com/WinRTWcfService/VSLiveWcfDataService.svc/";
#endif

In order for you to test the service locally, you have to enable localhost communication in your metro application project properties by checking Allow Local Network Loopback.

image

You will also enable internet communication and networking in your appxmanifest.  Just double click on your .manifest file and go to capabilities section.

image

Cool.  I am ready to load a collection of sessions for my list box control

async public void LoadData(CoreDispatcher dispatcher)
        {
            try
            {
                IsBusy = true;
                _dispatcher = dispatcher;

                var query = (DataServiceQuery<VSLiveWinRTData.ODataService.Session>)_oDataClient.Sessions.OrderBy(session => session.Title);
                var data = await query.ExecuteAsync();

                Sessions = new DataServiceCollection<VSLiveWinRTData.ODataService.Session>(_oDataClient);

                Sessions.Load(data);

                IsBusy = false;

            }
            catch (DataServiceClientException ex)
            {
                // handle error message
            }
        }

ExecuteAsync is an extension method on DataServiceQuery class:

    public static class WcfDataServicesExtensions
    {
        public static async Task<IEnumerable<TResult>> ExecuteAsync<TResult>(this DataServiceQuery<TResult> query)
        {
            var queryTask = Task.Factory.FromAsync<IEnumerable<TResult>>(query.BeginExecute(null, null), (asResult) =>
            {
                var result = query.EndExecute(asResult).ToList();
                return result;
            });

            return await queryTask;
        }
    }

I stole this code from Phani’s blog

http://blogs.msdn.com/b/phaniraj/archive/2012/04/26/developing-windows-8-metro-style-applications-that-consume-odata.aspx

I am storing the list in Sessions property of my view model:

        private DataServiceCollection<VSLiveWinRTData.ODataService.Session> _sessions;

        public DataServiceCollection<VSLiveWinRTData.ODataService.Session> Sessions
        {
            get { return _sessions; }
            set { _sessions = value; OnPropertyChanged("Sessions"); }
        }

DataServiceCollection inherits from ObservableCollection, so I do not need to do anything extra for data binding.  Once I make changes, I have to implement Save, which is almost identical to the code you would use in Silverlight or Windows Phone 7 applications.

        public void OnSave(object parameter)
        {
            IsBusy = true;
            _oDataClient.BeginSaveChanges(
             SaveChangesOptions.Batch,
             result =>
             {
                 _dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => {
                     if (result.IsCompleted)
                     {
                         var client = result.AsyncState as VSLiveWinRTData.ODataService.Context;
                         Debug.Assert(client != null, "client != null");
                         DataServiceResponse response = client.EndSaveChanges(result);
                         if (response.IsBatchResponse)
                         {
                             var errors = new StringBuilder();
                             foreach (ChangeOperationResponse change in response)
                             {
                                 if (change.Error != null)
                                 {
                                     errors.Append(change.Error.Message);
                                 }
                             }
                             if (errors.Length != 0)
                             {
                                 HasError = true;

                             }
                             else
                             {
                                 HasError = false;
                             }
                         }
                         IsBusy = false;
                     }
                 });
                
             }, _oDataClient);
        }

Code is fairly self-explanatory.  What is super cool about DataServiceCollection is that it keeps track of changes such as changes to existing items, deletions and additions.  So, after I make any changes, I just need to call exact same Save method.  Awesome, hah?  IsBusy and HasError properties are used in UI to show progress ring (former) or an error text block (latter).

 

Here is full View Model code just in case:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data.Services.Client;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Runtime.Serialization.Json;
using System.ServiceModel;
using System.Text;
using System.Threading.Tasks;
using VSLiveWinRTData.VSLiveService;
using Windows.Storage.Streams;
using Windows.UI.Core;
using Windows.UI.Xaml;

namespace VSLiveWinRTData
{
    public class WcfDataServicesViewModel : BaseClass
    {
#if DEBUG
        private const string _serviceUri = @"http://localhost/WinRTWcfService/VSLiveWcfDataService.svc/";
#else
        private const string _serviceUri = @"http://localhost/WinRTWcfService/VSLiveWcfDataService.svc/";
#endif

        private readonly VSLiveWinRTData.ODataService.Context _oDataClient;
        CoreDispatcher _dispatcher;

        public WcfDataServicesViewModel()
        {
            DeleteCommand = new SimpleCommand<VSLiveWinRTData.ODataService.Session>(OnDelete);
            SaveCommand = new SimpleCommand<object>(OnSave, CanSave);
            AddCommand = new SimpleCommand<object>(OnAdd);
            _oDataClient = new VSLiveWinRTData.ODataService.Context(new Uri(_serviceUri));
        }


        private DataServiceCollection<VSLiveWinRTData.ODataService.Session> _sessions;

        public DataServiceCollection<VSLiveWinRTData.ODataService.Session> Sessions
        {
            get { return _sessions; }
            set { _sessions = value; OnPropertyChanged("Sessions"); }
        }


        async public void LoadData(CoreDispatcher dispatcher)
        {
            try
            {
                IsBusy = true;
                _dispatcher = dispatcher;

                var query = (DataServiceQuery<VSLiveWinRTData.ODataService.Session>)_oDataClient.Sessions.OrderBy(session => session.Title);
                var data = await query.ExecuteAsync();

                Sessions = new DataServiceCollection<VSLiveWinRTData.ODataService.Session>(_oDataClient);

                Sessions.Load(data);

                IsBusy = false;

            }
            catch (DataServiceClientException ex)
            {
                // handle error message
            }
        }


        private bool isBusy;

        public bool IsBusy
        {
            get { return isBusy; }
            set { isBusy = value; OnPropertyChanged("IsBusy"); }
        }


        private VSLiveWinRTData.ODataService.Session selectedSession;

        public VSLiveWinRTData.ODataService.Session SelectedSession
        {
            get { return selectedSession; }
            set
            {
                selectedSession = value;
                OnPropertyChanged("SelectedSession");
            }
        }

        public SimpleCommand<VSLiveWinRTData.ODataService.Session> DeleteCommand { get; set; }

        public void OnDelete(VSLiveWinRTData.ODataService.Session parameter)
        {
            if (parameter != null)
            {
                if (parameter.SessionID > 0)
                {
                    IsBusy = true;

                    Sessions.Remove(parameter);
                    IsBusy = false;
                }
                else
                {
                    Sessions.Remove(parameter);
                    IsBusy = false;
                }
            }
        }

        public SimpleCommand<object> SaveCommand { get; set; }

        public void OnSave(object parameter)
        {
            IsBusy = true;
            _oDataClient.BeginSaveChanges(
             SaveChangesOptions.Batch,
             result =>
             {
                 _dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => {
                     if (result.IsCompleted)
                     {
                         var client = result.AsyncState as VSLiveWinRTData.ODataService.Context;
                         Debug.Assert(client != null, "client != null");
                         DataServiceResponse response = client.EndSaveChanges(result);
                         if (response.IsBatchResponse)
                         {
                             var errors = new StringBuilder();
                             foreach (ChangeOperationResponse change in response)
                             {
                                 if (change.Error != null)
                                 {
                                     errors.Append(change.Error.Message);
                                 }
                             }
                             if (errors.Length != 0)
                             {
                                 HasError = true;

                             }
                             else
                             {
                                 HasError = false;
                             }
                         }
                         IsBusy = false;
                     }
                 });
                
             }, _oDataClient);
        }

        public bool CanSave(object parameter)
        {
            return (_oDataClient != null);
        }

        private SimpleCommand<object> addCommand;

        public SimpleCommand<object> AddCommand
        {
            get { return addCommand; }
            set { addCommand = value; }
        }

        public void OnAdd(object parameter)
        {
            if (_oDataClient != null)
            {
                var session = new VSLiveWinRTData.ODataService.Session() { When = DateTime.Now };
                Sessions.Add(session);
                SelectedSession = session;
            }
        }

        private bool hasError;

        public bool HasError
        {
            get { return hasError; }
            set { hasError = value; OnPropertyChanged("HasError"); }
        }

      

    }
}

You can download the project here.  It also include other ways to persist the data in Metro applications, such as WCF (SOAP and REST), SQLite, Windows.Storage via files.  Please let me know if you have any questions.

2 Comments

  1. Sergey, great article, but I have one follow up question. How do you handle a WCF call when there is a Continuation from the data services? My service has a cap of 25 records, so I am not sure how this works in Win 8.

    On WP7, I handled this in my Load_Completed event and checked to see if there what a continuation. Any thoughts on how to do it with your code?

  2. Thank you so much!!!

    Heads up people! -> I had to change DataServiceClientException to DataServiceQueryException in order to keep from crashing.

    I wish Microsoft would have put out a WCF sample this good for Windows Store Apps.

Leave a Reply

Your email address will not be published. Required fields are marked *