Click here to Skip to main content
15,860,859 members
Articles / Programming Languages / C# 4.0

Introduction to ADO.NET Data Services/RIA Services

Rate me:
Please Sign up or sign in to vote.
4.93/5 (51 votes)
25 Feb 2010CPOL26 min read 179.3K   2.5K   166   51
An introduction to ADO.NET Data Services / RIA Services.

Table of Contents

Introduction

It has been a while since I penned my last article, which I can only apologize for; see, I have been moving house, do not even have internet at my new house yet (in fact, I have not even unpacked yet), so it has been slow progress with my trying to do a bit here and a bit there on the train with the trusty laptop.

Anyway, to cut a long story short, here is a new article. This article is not that complex, and will not give you any re-usable code in any shape, way, or form, but what it will hopefully do is give you a gentle introduction in how to get started with two of Microsoft's newer data access offerings:

  • WCF Data Services, formerly ADO.NET Data Services, formerly Astoria.
  • RIA Data Services (the new kid on the block).

I am no expert in these two technologies; as many of you will know, my main areas are WPF/LINQ/WCF/Threading, but I have not done much Silverlight since I wrote this article back in the day, so I am just having a dig about with what you can do with Silverlight these days, and have chosen to look at the data access side of things, and write a few things about what I found along the way.

Like I say, this is not an advanced article for people that may have used these two data access areas for some time; rather, it is a general overview of what can be done with these two different data access technologies and how you might go about doing some simple CRUD operations with them.

I should also point out that even though the attached demo code makes use of Silverlight, I have chosen, for the sake of brevity, to avoid complicating the reading experience by using the MVVM pattern. Even though I love that pattern, it is just not the point this article is trying to convey, so it's code-behind or XAML for this article's demo code, all the way. Please forgive me.

Prerequisites

I wrote the attached demo code using Visual Studio 2010 BETA 2 / Silverlight 4 / RIA Services Preview, so the following are all considered to be prerequistes:

And since both the ADO.NET Data Services code and RIA Services code make use of SQL data, SQL Server is also a prerequisite.

Database Setup

In fact, just while we are talking about SQL, let me explain how you will need to setup your SQL Server database in order to run the demo app.

If you open up the attached solution, you will see a solution folder which holds several setup scripts that allow you to create the demo database:

Image 1

You will need to run these SQL scripts against your own SQL Server installation, and the order to run these SQL scripts in is as follows:

  1. CreateDatabase.sql
  2. CreateTables.sql
  3. Add Some Dummy Data.sql

Database Connection

After you have created the demo apps database, you must remember to change the connection string settings in the following two Web.Config to point to your SQL Server installation. You may need to change the connection string DataSource/User ID and Password to match your SQL Server installation.

  1. RIADataServicesDemoApp.Web project, Web.Config, DemoEntities connection strings section
  2. WCFDataServicesDemoApp.Web project, Web.Config, DemoEntities connection strings section

A Word About the Demo App

The attached demo app is by no means a real world example, which may not please some people, but I did not want to create this huge overly complicated monster app when the basic CRUD principles could be demonstrated with a very easy database setup. To this end, the demo app's database contains two simple tables; in fact, the demo code actually only uses one of these two tables, but you know what? That is enough to demonstrate the basic CRUD operations of both ADO.NET Data Services and RIA Data Services.

The overall database looks like this:

Image 2

ADO.NET Data Services

What's It All About

ADO.NET Data Services were previously called "Astoria", and put plainly, are used to expose data as resources (JSON/ATOM) using REST. As such, the data is exposed as resources, and can be fetched using standard HTTP URLs. It is also possible to delete, update, and insert new data using REST. The RESTful resources are exposed using WCF under the covers, though looking at a typical ADO.NET Data Service,s you may not think so.

Since WCF is used as the actual service implementation, all the core logic that fetches data from the database is run on an application server, so can be behind a firewall, which helps maintain that nice three levels of physical secure separation that we may all be used to.

I think this diagram explains it fairly well, where you can imagine the database is on one box, the Data Access Layer (the ADO.NET Data Service) is on another physical box, and a client app (such as Silverlight) may be running on yet another different box (basically, the client's PC).

Image 3

By exposing the relational data using REST, we are able to use standard HTTP requests and make use of various URLs to access the resources. We are also able to use standard HTTP verbs such as PUT/POST to update the resources, which in the end may translate into updating a row of relational data in the relational database (if you are actually using a relational database to expose the data in the first place, which you may not be).

But before we get into that, we need to understand how to create an ADO.NET Data Service. So let's go through that, shall we?

Step 1

Is to define an Entity Model for the data. This is as easy (well, for the demo app, it is anyway) as creating a new Entity Model, and following the wizard through to completion, using your own SQL Server database connection string, and pointing to the WCFDataServicesDemoApp database you set up using the setup scripts provided and discussed above.

Image 4

Step 2

Once we have an Entity Model, we can then create a new ADO.NET Data Service to expose the relational data as resources. Again, this is easy; all we need to do is add a new item as shown below:

Image 5

Once you have done that, you will have a new <YOUR_SERVICE_NAME>.svc item in your solution with the following boilerplate code in it:

C#
using System;
using System.Collections.Generic;
using System.Data.Services;
using System.Data.Services.Common;
using System.Linq;
using System.ServiceModel.Web;
using System.Web;

namespace WCFDataServicesDemoApp.Web
{
    public class WebDataService1 : 
        DataService< /* TODO: put your data source class name here */ >
    {
        // This method is called only once to initialize service-wide policies.
        public static void InitializeService(DataServiceConfiguration config)
        {
            // TODO: set rules to indicate which entity sets and service 
            // operations are visible, updatable, etc.
            // Examples:
            // config.SetEntitySetAccessRule("MyEntityset", 
            //    EntitySetRights.AllRead);
            // config.SetServiceOperationAccessRule("MyServiceOperation", 
            //    ServiceOperationRights.All);
            config.DataServiceBehavior.MaxProtocolVersion = 
                DataServiceProtocolVersion.V2;
        }
    }
}

Believe it or not, you are nearly there with creating a basic ADO.NET Service. All you have to do is add the generic argument, which is the type of the EntityModel you created earlier, and decide which of your EntitySets you want to expose and what rights you want them to have. So for the demo app (which is an insanely trivial example), this looks like this:

C#
using System;
using System.Collections.Generic;
using System.Data.Services;
using System.Data.Services.Common;
using System.Linq;
using System.ServiceModel.Web;
using System.Web;

namespace WCFDataServicesDemoApp.Web
{
    public class WCFDataService : DataService<DemoAppEntities>
    {
        // This method is called only once to 
        // initialize service-wide policies.
        public static void InitializeService(
        DataServiceConfiguration config)
        {
            config.SetEntitySetAccessRule("*", 
                   EntitySetRights.All);
            config.DataServiceBehavior.MaxProtocolVersion = 
                   DataServiceProtocolVersion.V2;
        }


        // Define a change interceptor for the Address entity set.
        [ChangeInterceptor("Addresses")]
        public void OnChangeAddresses(Address address, 
            UpdateOperations operations)
        {
            if (operations == UpdateOperations.Add ||
               operations == UpdateOperations.Change)
            {

                if (String.IsNullOrEmpty(address.City) ||
                    String.IsNullOrEmpty(address.Line1) ||
                    String.IsNullOrEmpty(address.PostCode))
                {
                    throw new DataServiceException(400,
                       "Can not save Address with empty City/Line1/PostCode");
                }
            }
        }

    }
}

Do not worry too much about the OnChangeAddresses() method, I will get to that later. With this in place, we should be able to test this service out in the browser. We can do that using a number of URLs; let's see some: http://localhost:1183/WCFDataService.svc/ yields this:

XML
<?xml version="1.0" encoding="utf-8" standalone="yes" ?> 
 <service xml:base="http://localhost:1183/WCFDataService.svc/" 
   xmlns:atom="http://www.w3.org/2005/Atom" 
   xmlns:app="http://www.w3.org/2007/app" xmlns="http://www.w3.org/2007/app">
 <workspace>
  <atom:title>Default</atom:title> 
 <collection href="Addresses">
  <atom:title>Addresses</atom:title> 
  </collection>
 <collection href="Customers">
  <atom:title>Customers</atom:title> 
  </collection>
  </workspace>
  </service>

We can see the ADO.NET Service is alive and kicking. Note the 2 x collection nodes in there: we have one for Addresses and one for Customers. Wonder what happens when we try one of these URLs. Shall we try one?

How about this one: http://localhost:1183/WCFDataService.svc/Addresses/? See how we get a list of all the addresses:

Image 6

Pretty cool, huh? But what else can we do? Well, actually, we can do a whole lot more; for example, try some of these URLs:

  • http://localhost:1183/WCFDataService.svc/Addresses(6)/: Gets the address with AddressId = 6
  • http://localhost:1183/WCFDataService.svc/Addresses?$top=2: Gets the top two addresses
  • http://localhost:1183/WCFDataService.svc/Addresses?$orderby=City%20asc: Gets all addresses ordered by city, ascending

For more information on these query strings, I found the following article very useful: http://msdn.microsoft.com/en-us/library/cc907912.aspx.

So that shows you what is happening from the service point of view, but how do you go about using this as a client? Luckily, there is a great Silverlight Client API to work with, which is what we shall talk about next.

Before we do that, it is important to note that since the resources are exposed using JSON/ATOM, they can easily be used by non-Microsoft clients. JSON obviously means that any JavaScript could easily consume an ADO.NET Data Service, which I think is one of its main selling points. It is a simple, no gloss way to easily expose data using WCF without the need to get into the nitty gritty of WCF. If any of you would like to see a true RESTful WCF article, I published one some time back at the following URL, so you may like to compare how much infrastructure work I had to go through to allow CRUD operations using the RESTFul WCF extensions back then: http://www.codeproject.com/KB/smart/GeoPlaces.aspx.

Creating Data

Right, so now it is time to see how we can actually consume some of the REST exposed resources in a client app. Obviously, as I am into Microsoft technologies, I will be using Silverlight. The demo app uses Silverlight 4.0. How does Silverlight make use of the RESTful resources exposed from ADO.NET Data Services? Well, as I previously mentioned, there is a very nice Client API, which you get when you download and install the ADO.NET Services installer (I gave out a link at the start of the article). The Client API is a single DLL called Microsoft.Data.Services.Client.dll, and can be found at the following location (assuming you downloaded and installed the ADO.NET Services installer using the default settings): C:\Program Files\ADO.NET Data Services V1.5 CTP1\sl_bin.

Anyway, once you have the Microsoft.Data.Services.Client.dll installed and referenced by your Silverlight application, you are ready to rumble. Let's crack on and have a look at cresting some data, shall we? But just before that, I want to talk you through how the demo app looks and works, just so you know how to drive it. I have to say it is not pretty, but it does the job.

Image 7

The screen is divided into four areas, which do the following:

  • Top left: Shows all addresses.
  • Bottom left: Shows all addresses where city starts with "B".
  • Top right: Allows users to update/delete an item. The user must select an item to update/delete from the list of all addresses within the top left list of addresses.
  • Bottom right: Allows a user to create a new address. To do this, the user must click "Create Fresh Item" and then fill in the data, and then click the "Insert" button.

Just before I show you to get going with the programmatic methods involved with the basic CRUD operations, I should just mention that I did run into a small cross access domain issue, and I got around this by including the following files within both the ADO.NET Data Services web app and the RIA Services web app in the attached code. I did not author these myself, but found them on the internet, and they worked for me.

ClientAccessPolicy.xml
XML
<?xml version="1.0" encoding="utf-8"?>
<access-policy>
  <cross-domain-access>
    <policy>
      <allow-from http-request-headers="*">
        <domain uri="*"/>
      </allow-from>
      <grant-to>
        <resource path="/" include-subpaths="true"/>
      </grant-to>
    </policy>
  </cross-domain-access>
</access-policy>
CrossDomain.xml
XML
<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy 
   SYSTEM "http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
  <allow-http-request-headers-from domain="*" headers="*"/>
</cross-domain-policy>

OK, so now that you know what the demo app can do, and how to deal with the cross domain issue, how about we get started? The very first thing we need to do in our Silverlight app is create a pointer that points to our ADO.NET Data Service. This can be done as follows:

C#
//a link to our WCF data service
DemoAppEntities proxy = new DemoAppEntities(
    new Uri("http://localhost:1183/WCFDataService.svc", UriKind.Absolute));

From there, we can create some relational data like this (remember, this will be doing a POST HTTP request really):

C#
private void Insert_Click(object sender, RoutedEventArgs e)
{
    Insert(insertAddress);
    btnInsert.IsEnabled = false;
}

private void Insert(Address currentAddress)
{
    proxy.AddToAddresses(currentAddress);

    proxy.BeginSaveChanges(new AsyncCallback(r =>
    {
        try
        {
            proxy.EndSaveChanges(r);
            FetchAllAddresses();
            FetchAllBAddresses();
        }
        catch (Exception ex)
        {
            MessageBox.Show(String.Format("Error Inserting\r\n{1}",  
                ReadException(ex.InnerException.Message.ToString())));
        }
    }), null);
}

private string ReadException(String error)
{
    XDocument xdoc = XDocument.Parse(error);
    return ((XElement)xdoc.DescendantNodes().First()).Value;
}

As you can see, the client API allows us to simply add a new address by using a proxy that we created earlier. Of course, this being Silverlight, we need to do this asynchronously. The only other thing to note there is that we are catching an Exception and then parsing that using some XLINQ. What is that all about?

Well recall this bit of code in the demo app's actual ADO.NET Service definition that I said I would get back to:

C#
// Define a change interceptor for the Address entity set.
[ChangeInterceptor("Addresses")]
public void OnChangeAddresses(Address address, 
    UpdateOperations operations)
{
    if (operations == UpdateOperations.Add ||
       operations == UpdateOperations.Change)
    {

        if (String.IsNullOrEmpty(address.City) ||
            String.IsNullOrEmpty(address.Line1) ||
            String.IsNullOrEmpty(address.PostCode))
        {
            throw new DataServiceException(400,
               "Can not save Address with empty City/Line1/PostCode");
        }
    }
}

Well, that is where the Exception comes from. But, without that small bit of parsing done with XLINQ, we would get a vast chunk of XML. The XML (as it's easy to send via HTTP). Let me show you what it would contain in full before being parsed to extract just the actual Exception message:

XML
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<error xmlns="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
  <code></code>
  <message xml:lang="en-GB">Can not save Address with empty City/Line1/PostCode</message>
</error>

So that is what the XLINQ was all about, but what about the usage of the ChangeInterceptorAttribute in the actual ADO.NET Service implementation? Well, here is what MSDN has to say about using Interceptors, in general:

ADO.NET Data Services enables an application to intercept request messages so that you can add custom logic to an operation. You can use this custom logic to validate data in incoming messages. You can also use it to further restrict the scope of a query request, such as to insert a custom authorization policy on a per request basis.

Interception is performed by specially attributed methods in the data service. These methods are called by ADO.NET Data Services at the appropriate point in message processing. Interceptors are defined on a per-entity set basis, and interceptor methods cannot accept parameters from the request like service operations can. Query interceptor methods, which are called when processing an HTTP GET request, must return a lambda expression that determines whether an instance of the interceptor's entity set should be returned by the query results. This expression is used by the data service to further refine the requested operation. The following is an example definition of a query interceptor.

http://msdn.microsoft.com/en-us/library/dd744842.aspx

If you want to see the ChangeInterceptor in action in the demo app, just try and insert a new address with a missing bit of data and watch what happens. You should see a MessageBox similar to the following shown:

Image 8

Retrieving Data

Retrieving data is actually pretty cool, as thanks to the groovy Client API, we can use a subset of LINQ to tailor our queries to what we would like to fetch. The demo app has two queries: one that fetches all addresses, and one that fetches only addresses whose city begins with "B". Let's have a look at those, shall we?

All Addresses

Pretty standard stuff here; do a simple fetch of all addresses (again, must be asynchronous as we are using Silverlight):

C#
private void FetchAllAddresses()
{
    // Create the query using LINQ
    var qry = proxy.Addresses;

    // Execute the Query
    qry.BeginExecute(new AsyncCallback(r =>
    {
        try
        {
            lstAllAddresses.ItemsSource = qry.EndExecute(r);
        }
        catch (Exception ex)
        {
            MessageBox.Show("Error FetchAllBAddresses\r\n" +
                ReadException(ex.InnerException.Message.ToString()));
        }
    }), null);
}

B Addresses

This time, look at what we can do. We can use some LINQ to totally tailor what we want to fetch (again, must be asynchronous as we are using Silverlight):

C#
private void FetchAllBAddresses()
{
    //use some projections that were added in ADO .NET DataServices v1.5
    var qry = (from a in proxy.Addresses
               where a.City.ToLower().StartsWith("b")
               orderby a.City
               select a) as DataServiceQuery<Address>;

    // Execute the projected Query
    qry.BeginExecute(new AsyncCallback(r =>
    {
        try
        {
            lstCityBAddresses.ItemsSource = qry.EndExecute(r);
        }
        catch (Exception ex)
        {
            MessageBox.Show("Error FetchAllBAddresses\r\n" + 
                ReadException(ex.InnerException.Message.ToString()));
        }
    }), null);
}

Updating Data

This works in much the same way as an Insert does; in fact, in the demo app's code, the Insert/Update/Delete all use a common method, as the only thing that is different is the operation type (Insert/Update/Delete). I am just going to show that common method here, but Delete works with this common method too.

C#
private void Update_Click(object sender, RoutedEventArgs e)
{
    DeleteOrUpdateOrInsert(OperationType.Update, 
      (Address)lstAllAddresses.SelectedItem);
}

private void Delete_Click(object sender, RoutedEventArgs e)
{
    DeleteOrUpdateOrInsert(OperationType.Delete, 
      (Address)lstAllAddresses.SelectedItem);
}


private void DeleteOrUpdateOrInsert(OperationType opType,
             Address currentAddress)
{

    switch (opType)
    {
        case OperationType.Delete:
        case OperationType.Update:
            if (currentAddress == null)
            {
                MessageBox.Show(
                  "You need to pick an address from All Addresses 1st");
                return;
            }
            break;
    }

    String msgType = String.Empty;
    switch (opType)
    {
        case OperationType.Delete:
            proxy.DeleteObject(currentAddress);
            msgType = "Deleting";
            break;
        case OperationType.Update:
            proxy.UpdateObject(currentAddress);
            msgType = "Updating";
            break;
        case OperationType.Insert:
            proxy.AddToAddresses(currentAddress);
            msgType = "Inserting";
            break;
    }

    proxy.BeginSaveChanges(new AsyncCallback(r =>
    {
        try
        {
            proxy.EndSaveChanges(r);
            FetchAllAddresses();
            FetchAllBAddresses();
        }
        catch (Exception ex)
        {
            MessageBox.Show(String.Format("Error {0}\r\n{1}", msgType, 
                ReadException(ex.InnerException.Message.ToString())));
        }
    }), null);
}

private string ReadException(String error)
{
    XDocument xdoc = XDocument.Parse(error);
    return ((XElement)xdoc.DescendantNodes().First()).Value;
}

Deleting Data

Uses the same method as Update, but Deletes current object rather than Updates it, see the demo app's code.

Validation

Whilst there is no current validation per say, Shawn Wildermuth has started an Open Source project that is intended to bring RIA style validation to ADO.NET Data Services; it is a very interesting project that involves generating code base and creating proxies. Have a look at it at the following URL: http://niagara.codeplex.com/.

It is a very interesting read, and is the first proper use of OSLO that I have seen. Kudos to Shawn.

RIA Data Services

What's It All About

RIA Data Services are kind of the new kid on the block. They too offer the ability to abstract an EntityModel behind a WCF Service. So why not use ADO.NET Data Services? Well, RIA Data Services also has a few tricks up its sleeves. As we previously saw, the only form of in-built validation support in ADO.NET Data Services seems to be to use Interceptors, which pretty much look like they are really intended to work with client apps that can easily parse the XML contained within the Exception. I don't know about you, but I am not that keen on parsing a bit of XML to get an Exception message. Perhaps I am stupid and missing something, who knows.

RIA, on the other hand, uses some neat dynamic data inspired tricks to allow the EnityModel entities to be marked up with extra validation attributes (System.ComponentModel.DataAnnotations namespace) which actually work on the Silverlight client app. By using these validation attributes, you are able to adorn a partial metadata class with metadata that will be used to validate a business object on the client.

If you have ever done any WPF work, think IDataErrorInfo.

The DomainService is really just a WCF Service, and it will format the data based on your endpoint. Silverlight uses the binary endpoint (which is just regular WCF binary), but RIA Services also comes with a SOAP endpoint and a JSON endpoint. The real difference between Data Services and RIA Services is that Data Services sends object graphs (i.e., on the client, you get ObjectA.ObjectB.ObjectC) while RIA Services sends individual objects (ObjectA, ObjectB, ObjectC) and expects the client to put the objects back together by matching up foreign keys. (i.e., ObjectB.ObjectAId = ObjectA.Id). This means that with RIA Services, the client must know the structure of the objects in advance to be able to put things back together again, while with Data Services, the structure of the data can be inferred at runtime on the client.

Shawn Wildermuth has an excellent blog post on ADO.NET Data Services that I would urge you all to read around, where he does a great job of talking about the benefits of each of these data access approaches: http://wildermuth.com/Tag/ADO.NET%20Data%20Services.

To give you the same sort of view that I did when I talked about ADO.NET Data Services, I think this figure illustrates what is going on quite well:

Image 9

Shamelessly taken from Nikhil's blog.

So I guess you want to see some code, right?

But before we get into that, we need to understand how to create a RIA Data Service. Let's go through that, shall we?

Step 1

Is to define an Entity Model for the data. This is as easy (well, for the demo app, it is anyway) as creating a new Entity Model and following the wizard through to completion, using your own SQL Server database connection string, and pointing to the WCFDataServicesDemoApp database you set up using the setup scripts provided and discussed above.

Image 10

Step 2

Once we have an Entity Model, we need to compile the code so that the RIA Service wizard will see that there indeed is an EntityModel that can be exposed over a RIA Service. So once you have built the code, it is really just a matter of adding in a RIA Service, which can be done using the following wizard:

Image 11

Then you will be asked if you want to attach this RIA Service instance to an EntityModel, which you can again choose the demo app's database (Step 1), and pick what sort of access you would like to give the EnityModel within the RIA Service instance.

Image 12

Once you have done that, you will have a new <YOUR_SERVICE_NAME>.cs item in your solution with the following boilerplate code in it. You are free to alter this boiler plate code to add new methods as you see fit.

I should point out that I am using the SL4 BusinessApplication VS2010 template that gives you the Web project and a Silverlight 4 RIA project, where the SL4 application also has all the necessary navigation bits and pieces. It is quite a time saver, so it is well worth using. The only issue is that there are a lot of things you may not need in there; for the sake of this demo application, I have deleted a lot of the unwanted stuff, and just left in what was required to run the demo code.

Anyway, the attached demo code's RIA Service looks like this (where I generated and modified it using the same EntityModel options as I just showed in the figure above):

C#
namespace RIADataServicesDemoApp.Web.CustomRIAServices
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.ComponentModel.DataAnnotations;
    using System.Data;
    using System.Linq;
    using System.Web.DomainServices;
    using System.Web.DomainServices.Providers;
    using System.Web.Ria;
    using System.Web.Ria.Services;
    using RIADataServicesDemoApp.Web.Model;

    [EnableClientAccess()]
    public class RIADataService : 
           LinqToEntitiesDomainService<RIADemoAppEntities>
    {

        public IQueryable<Address> GetAddresses()
        {
            return this.ObjectContext.Addresses.OrderBy(x => x.AddressId);
        }

        public void InsertAddress(Address address)
        {
            if ((address.EntityState != EntityState.Added))
            {
                if ((address.EntityState != EntityState.Detached))
                {
                    this.ObjectContext.ObjectStateManager.
                        ChangeObjectState(address, EntityState.Added);
                }
                else
                {
                    this.ObjectContext.AddToAddresses(address);
                }
            }
        }

        public void UpdateAddress(Address currentAddress)
        {
            if ((currentAddress.EntityState == EntityState.Detached))
            {
                this.ObjectContext.AttachAsModified(currentAddress, 
                    this.ChangeSet.GetOriginal(currentAddress));
            }
        }

        public void DeleteAddress(Address address)
        {
            if ((address.EntityState == EntityState.Detached))
            {
                this.ObjectContext.Attach(address);
            }
            this.ObjectContext.DeleteObject(address);
        }

        public IQueryable<Customer> GetCustomers()
        {
            return this.ObjectContext.Customers;
        }

        public void InsertCustomer(Customer customer)
        {
            if ((customer.EntityState != EntityState.Added))
            {
                if ((customer.EntityState != EntityState.Detached))
                {
                    this.ObjectContext.ObjectStateManager.
                        ChangeObjectState(customer, EntityState.Added);
                }
                else
                {
                    this.ObjectContext.AddToCustomers(customer);
                }
            }
        }

        public void UpdateCustomer(Customer currentCustomer)
        {
            if ((currentCustomer.EntityState == EntityState.Detached))
            {
                this.ObjectContext.AttachAsModified(currentCustomer, 
                    this.ChangeSet.GetOriginal(currentCustomer));
            }
        }

        public void DeleteCustomer(Customer customer)
        {
            if ((customer.EntityState == EntityState.Detached))
            {
                this.ObjectContext.Attach(customer);
            }
            this.ObjectContext.DeleteObject(customer);
        }
    }
}

Again, we are nearly there with creating a basic working RIA Service, albeit a far from real world example, but for the sake of basic CRUD operations, it will do.

But just before that, I want to talk you through how the demo app looks and works, just so you know how to drive it. I have to say, it is not pretty, but it does the job. I should point out right now that the RIA demo app does the same job as the previously demonstrated ADO.NET Data Services app, which as I have stated all through out this article, is not breath taking or real world, but should demonstrate simple CRUD operations a-ok.

Image 13

The screen is divided into four areas, which do the following:

  • Top left: Shows all addresses.
  • Bottom left: Shows all addresses where city starts with "B".
  • Top right: Allows users to Update/Delete an item. The user must select an item to Update/Delete from the list of all addresses within the top left list of addresses.
  • Bottom right: Allows a user to Create a new address. To do this, the user must click "Create Fresh Item" and then fill in the data, and then click the "Insert" button.

As I stated before, I am using the SL BusinessApplication template which has a lot of files, but all the stuff you would be interested in for the demo app is within the Views\Home.xaml and Views\Home.xaml.cs files.

Retrieving Data

If you have ever done any work with either LINQ to SQL or LINQ to Entities, you will be familiar with the idea of a DataContext object that has entities and entity sets/relationships of some sort, and exposes these as properties and collections of some sort of a central context object, and has the inbuilt ability to track changes, and also allows the user to submit changes back to the database by calling methods such as Addresses.Add() and SubmitChanges().

If this all seems pretty familiar to you, you will be pleased to know that RIA Services has the same sort of Client API, as you will see in the coming sections.

So let's start off with some data retrieval, shall we?

We could do something like we did with Data Services, just like this:

C#
//has LINQ extension methods for EntityQuery<T>
using System.Windows.Ria; 

//This is how you can also bind stuff using code behind for RIA services
try
{
    RIADataContext context = new RIADataContext();
    var qry = context.GetAddressesQuery().Where(a => 
    a.City.ToLower().StartsWith("b"));

    // Bind the data
    lstCityBAddresses.ItemsSource = 
    context.Load<Address>(qry).AllEntities;
}
catch (Exception ex)
{
}

But with RIA, things get a little more interesting. We can, in fact, do a lot of stuff straight in the XAML using some very rich data controls that come with RIA. Let's have a look at some of those, shall we?

The attached RIA demo does exactly the same thing as the ADO.NET Data Services app, so let's continue.

All Addresses List

This is done using some RIA controls, which is defined in XAML as follows:

XML
<riaControls:DomainDataSource 
    x:Name="allAddressesDataSource"
    QueryName="GetAddressesQuery" 
    AutoLoad="True"/>

<ListBox x:Name="lstAllAddresses" 
         Grid.Row="1" 
         ItemTemplate="{StaticResource addTemplate}"
         ItemsSource="{Binding Data, 
            ElementName=allAddressesDataSource}"/>

Where the allAddressesDataSource DomainDataSource has its Context set in code-behind, as follows:

C#
private RIADataContext context = new RIADataContext();

public Home()
{
  InitializeComponent();
  this.allAddressesDataSource.DomainContext = context;
}

Also note that the allAddressesDataSource DomainDataSource has a QueryName property which is set to "GetAddressesQuery"; where does this come from?

Well, if we go back to our original RIA Service and have a look, there is a IEnumerable<Address>; that is the source for the data that the RIA DomainDataSource control is using.

C#
[EnableClientAccess()]
public class RIADataService : LinqToEntitiesDomainService<RIADemoAppEntities>
{

    public IQueryable<Address> GetAddresses()
    {
        return this.ObjectContext.Addresses.OrderBy(x => x.AddressId);
    }
}

B Addresses List

This is done using some RIA controls, which is defined in XAML as follows:

XML
<riaControls:DomainDataSource 
    x:Name="allBStartingAddressesDataSource"
        QueryName="GetAddressesQuery" AutoLoad="True">

    <riaControls:DomainDataSource.FilterDescriptors>
        <riaData:FilterDescriptorCollection 
        x:Name="DataSourceFilters" 
        LogicalOperator="And">
            <riaData:FilterDescriptor 
            PropertyPath="City" 
            Operator="StartsWith">
                <!--
                NOTE : There appears to be an issue with the 
               controlParameter, having a direct
                       Value, so have been forced into using a 
                       control to bind to for ControlParameter
                       Value
                -->
                <riaControls:ControlParameter  
            ControlName="txtCityFilter" 
                        PropertyName="Text" />
            </riaData:FilterDescriptor>
        </riaData:FilterDescriptorCollection>
    </riaControls:DomainDataSource.FilterDescriptors>

    <riaControls:DomainDataSource.SortDescriptors>
        <riaData:SortDescriptor PropertyPath="City" 
        Direction="Ascending" />
    </riaControls:DomainDataSource.SortDescriptors>
    
</riaControls:DomainDataSource>

<ListBox x:Name="lstCityBAddresses" Grid.Row="4" 
         ItemsSource="{Binding Data, 
        ElementName=allBStartingAddressesDataSource}"
         ItemTemplate="{StaticResource addTemplate}"/>

Notice this time, there is a bit more going on; the DomainDataSource control is a bit more elaborate. We are actually setting up some FilterDescriptors and SortDescriptors right there in the XAML. That is something that RIA offers that ADO.NET Data Services do not, a rich set of controls. However, as a result, there is a tighter coupling in RIA than in ADO.NET Data Services.

Anyway, it can be seen from the XAML above that we are declaring a FilterDescriptor that is filtering a list of some objects, where the City property StartsWith some value. Where the value is bound to a control called "txtCityFilter", which in the demo app is simply set to the string "B". The DomainDataSource control's context is the same IEnumerable<Addresses> from the RIA Service as before; it's just this time, we are filtering this list on the client side using these rich RIA controls.

Note: I think there is a small bug in the current RIA version out there in that I could not set a string literal of say "B" for the Value property of the ControlParameter directly in the XAML. I had to use a binding to another control. I would not expect this defect to last long.

Creating Data

Creating data is pretty easy; it is just a matter of adding a new item to the relevant context EntitySet<T>. Here is the code that adds a new address:

C#
context.Addresses.Add(newAddress);
context.SubmitChanges();

Updating Data

Updating data is also pretty easy. As with the ADO.NET Data Services demo app, I have a combined helper method in the code-behind that deals with either doing a Delete or an Update, and here is that simple helper method, where it can be seen that the first thing that happens is that there is a check to see whether there is a currently selected item within the list of all addresses.

If the operation type is Delete, the selected address is removed from the context (and therefore the database). If the operation type is Update, the next thing that happens is that the DataForm that is being used to display the currently selected address is validated, and only if it's valid will the operation complete.

C#
private void Delete_Click(object sender, RoutedEventArgs e)
{
    DeleteOrUpdate(OperationType.Delete, 
    (Address)editAddressDataForm.CurrentItem);
}

private void Update_Click(object sender, RoutedEventArgs e)
{
    DeleteOrUpdate(OperationType.Update, 
    (Address)editAddressDataForm.CurrentItem);
}

private void DeleteOrUpdate(OperationType opType, 
    Address currentAddress)
{
    switch (opType)
    {
        case OperationType.Delete:
        case OperationType.Update:
            if (currentAddress == null)
            {
                MessageBox.Show(
            "You need to pick an address from All Addresses 1st");
                return;
            }
            break;
    }


    switch (opType)
    {
        case OperationType.Delete:
            context.Addresses.Remove(currentAddress);
            context.SubmitChanges();
            break;
        case OperationType.Update:
            if (editAddressDataForm.ValidateItem())
            {
                //Could not get the XAML Parser to 
        //understand {x:Type dataForm:DataForm.ValidationSummary} 
                //for a XAML based Style
                if (editAddressDataForm.ValidationSummary != null)
                    editAddressDataForm.ValidationSummary.Foreground 
                       = new SolidColorBrush(Colors.Red);

                if (editAddressDataForm.IsItemValid)
                {
                    editAddressDataForm.CommitEdit();
                    if (context.HasChanges)
                        context.SubmitChanges();
                }
            }
            break;
    }
}

Where the XAML looks like this:

XML
<dataForm:DataForm Grid.Row="0" x:Name="editAddressDataForm"   
                   CurrentItem="{Binding ElementName=lstAllAddresses, Path=SelectedItem}"
                   AutoGenerateFields="False" AutoCommit="True" 
                   AutoEdit="True" CommandButtonsVisibility="None" Margin="0,0,0,5" >
    <dataForm:DataForm.EditTemplate>
        <DataTemplate>
            <StackPanel>
                <dataForm:DataField Label="AddressId">
                    <TextBlock Text="{Binding AddressId, Mode=TwoWay,
            NotifyOnValidationError=True,  ValidatesOnExceptions=True}" />
                </dataForm:DataField>
                <dataForm:DataField Label="City">
                    <TextBox Text="{Binding City, Mode=TwoWay,
            NotifyOnValidationError=True,  ValidatesOnExceptions=True}" />
                </dataForm:DataField>
                <dataForm:DataField Label="Line1">
                    <TextBox Text="{Binding Line1, Mode=TwoWay,
            NotifyOnValidationError=True,  ValidatesOnExceptions=True}" />
                </dataForm:DataField>
                <dataForm:DataField Label="Line2">
                    <TextBox Text="{Binding Line2, Mode=TwoWay,
            NotifyOnValidationError=True,  ValidatesOnExceptions=True}" />
                </dataForm:DataField>
                <dataForm:DataField Label="PostCode">
                    <TextBox Text="{Binding PostCode, Mode=TwoWay,
            NotifyOnValidationError=True,  ValidatesOnExceptions=True}" />
                </dataForm:DataField>

            </StackPanel>
        </DataTemplate>
    </dataForm:DataForm.EditTemplate>
</dataForm:DataForm>

Deleting Data

This works in much the same way as Update, except that the call to the Context's EntitySet<Addresses> has a Remove for the Delete operation just before calling SubmitChanges(). Here is the code chunk that does a delete:

C#
private RIADataContext context = new RIADataContext();
...
...
context.Addresses.Remove(currentAddress);
context.SubmitChanges();

RIA Validation

RIA Validation is a double edged sword in that it is quite cool that you do in fact get validation support for your EnityModel objects on the client, which is all well and dandy, but at what cost? For example, consider these issues:

  • In my eyes, the EntityModel objects should be POCO objects, and the way the current RIAValidation works relies on using attributes within System.ComponentModel.DataAnnotations.dll, which makes it less POCO like and also less transportable.
  • The way validation is enabled is by the use of custom partial classes that use the attributes required to do the validation. It would be nice if this could come from other sources, such as XML, the database, but at present, it looks like hand cranked partial classes, or code generated classes, or T4 generated classes are the only way, as the System.ComponentModel.DataAnnotations.dll attributes are required in order to make the validations work in the Silverlight client.
  • MetadataTypeAttribute does feel like quite a hack. It works, but my, it's not that pretty, is it?

Anyway, all that said, my job in this article was to show what the current state of validation is within the RIA Services area, and that is precisely what I am going to do now.

So How Does the RIA Services Validation Work

Well, as we saw above, we have an actual RIA Service which has a context object, which is the EntityModel it is wrapping. The next piece of the puzzle is that we have a code file with Model partial classes (that match the EntityModel's model objects) that sits beside the RIA Service. This extra metadata file may contain one or more Model partial classes (that match the EntityModel's model objects), each of which should be marked up with MetadataTypeAttribute. This attribute is used to provide validation information that is used by the Silverlight client to ascertain which class holds the current validation attributes (which we will get to in a minute) for the current serialized RIA DataContext object.

We now know that there is a metadata file that contains one or more partial Model classes that have a MetadataTypeAttribute, so what else do these partial classes have going on? That can't be enough, can it? Well no, it can't!

The next piece of the puzzle is that we need to reference System.ComponentModel.DataAnnotations.dll in order to annotate the meta data with all the validation rules we want them to have. By doing this, we now have a tight coupling to System.ComponentModel.DataAnnotations.dll, but hey ho.

System.ComponentModel.DataAnnotations.dll does contain some very useful attributes, such as:

  • [Required]
  • [RegularExpression("[A-Z][A-Za-z0-9]*")]
  • [StringLength(32)]

You can see where that is going, I expect, so here is an example partial class that may be used with the Address Model from the demo app's EntityModel / RIA Service:

C#
#pragma warning disable 649
// disable compiler warnings about unassigned fields

namespace RIADataServicesDemoApp.Web.Model
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.ComponentModel.DataAnnotations;
    using System.Data.Objects.DataClasses;
    using System.Linq;
    using System.Web.DomainServices;
    using System.Web.Ria;
    using System.Web.Ria.Services;

    // The MetadataTypeAttribute identifies AddressMetadata as the class
    // that carries additional metadata for the Address class.
    [MetadataTypeAttribute(typeof(Address.AddressMetadata))]
    public partial class Address
    {

        // This class allows you to attach custom attributes to properties
        // of the Address class.
        internal sealed class AddressMetadata
        {

            // Metadata classes are not meant to be instantiated.
            private AddressMetadata()
            {
            }

            public int AddressId;

            [Required]
            [StringLength(20)]
            public string City;

            public EntityCollection<Customer> Customers;

            public EntityCollection<Customer> Customers1;

            [Required]
            [StringLength(20)]
            public string Line1;

            public string Line2;

            [Required]
            [StringLength(20)]
            public string PostCode;
        }
    }
}

#pragma warning restore 649    
// re-enable compiler warnings about unassigned fields

The final piece of the puzzle is very easy; it's just a matter of convention. You just need to get your bindings right on the Silverlight side, and all is cool. Here is an example where I am using a DataForm and have it bound to an Address that is being added to RIA:

XML
<dataForm:DataForm Grid.Row="0" x:Name="addAddressDataForm"   
                   AutoGenerateFields="False" AutoCommit="True" 
                   AutoEdit="True" CommandButtonsVisibility="None" 
                   Margin="0,0,0,5" >
    <dataForm:DataForm.EditTemplate>
        <DataTemplate>
            <StackPanel>
                <dataForm:DataField Label="City">
                    <TextBox Text="{Binding City, 
                    Mode=TwoWay,
                    NotifyOnValidationError=True,  
                    ValidatesOnExceptions=True}" />
                </dataForm:DataField>
                <dataForm:DataField Label="Line1">
                    <TextBox Text="{Binding Line1, 
                    Mode=TwoWay,
                    NotifyOnValidationError=True,  
                    ValidatesOnExceptions=True}" />
                </dataForm:DataField>
                <dataForm:DataField Label="Line2">
                    <TextBox Text="{Binding Line2, 
                    Mode=TwoWay,
                    NotifyOnValidationError=True,  
                    ValidatesOnExceptions=True}" />
                </dataForm:DataField>
                <dataForm:DataField Label="PostCode">
                    <TextBox Text="{Binding PostCode, 
                    Mode=TwoWay,
                    NotifyOnValidationError=True,  
                    ValidatesOnExceptions=True}" />
                </dataForm:DataField>

            </StackPanel>
        </DataTemplate>
    </dataForm:DataForm.EditTemplate>
</dataForm:DataForm>

The important parts of this are:

  • Mode=TwoWay: Which is crucial as the validation is actually done by hitting the server (web site app), and then coming back to the Silverlight client with any validation errors.
  • NotifyOnValidationError=True: Which tells the DataForms field to validate if an error occurs, a validation error that is; this is where the Annotations attributes are used.
  • ValidatesOnExceptions=True: Which tells the DataForms field to validate if an Exception occurs (last hope, like trying to parse a String "Tree" into an Int32 bound field, say).

To trigger a validation, here is what I do with the DataForm:

C#
if (editAddressDataForm.ValidateItem())
{
    //Could not get the XAML Parser to 
    //understand {x:Type dataForm:DataForm.ValidationSummary} 
    //for a XAML based Style
    if (editAddressDataForm.ValidationSummary != null)
        editAddressDataForm.ValidationSummary.Foreground 
        = new SolidColorBrush(Colors.Red);

    if (editAddressDataForm.IsItemValid)
    {
        editAddressDataForm.CommitEdit();

        switch (opType)
        {
            case OperationType.Delete:
                context.Addresses.Remove(currentAddress);
                context.SubmitChanges();
                break;
            case OperationType.Update:
                if (context.HasChanges)
                    context.SubmitChanges();
                break;
        }
    }
}

One point: it would, of course, be better to Style DataForm.ValidationSummary in XAML, but that just would not work for me, so I resorted to this small bit of code-behind to apply a red foreground brush. Again, I think it was me that was doing something wrong, I would expect this to be fixed soon.

Here is the result of trying to insert an incorrect address using RIA validation:

Image 14

That's All Folks

Anyways folks, that is all I have to say for now. As I have stated on numerous occasions in this article, this is by no means a real world app, nor will it make you a real world expert in WCF Data Services or RIA Services, but I think it has shown you the basics of both, and maybe given you a taste of what is possible with these two different data stacks, and what your current options are as far as Silverlight development goes.

On a slightly different tip, I am just about to become a dad for the first time (baby boy, er... future coder I mean), so this may be my last article for a little while (we shall see), but anyway, I will try and keep the Barberous code machine in full throttle, though it may be hard.

As always, votes/comments are welcome.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United Kingdom United Kingdom
I currently hold the following qualifications (amongst others, I also studied Music Technology and Electronics, for my sins)

- MSc (Passed with distinctions), in Information Technology for E-Commerce
- BSc Hons (1st class) in Computer Science & Artificial Intelligence

Both of these at Sussex University UK.

Award(s)

I am lucky enough to have won a few awards for Zany Crazy code articles over the years

  • Microsoft C# MVP 2016
  • Codeproject MVP 2016
  • Microsoft C# MVP 2015
  • Codeproject MVP 2015
  • Microsoft C# MVP 2014
  • Codeproject MVP 2014
  • Microsoft C# MVP 2013
  • Codeproject MVP 2013
  • Microsoft C# MVP 2012
  • Codeproject MVP 2012
  • Microsoft C# MVP 2011
  • Codeproject MVP 2011
  • Microsoft C# MVP 2010
  • Codeproject MVP 2010
  • Microsoft C# MVP 2009
  • Codeproject MVP 2009
  • Microsoft C# MVP 2008
  • Codeproject MVP 2008
  • And numerous codeproject awards which you can see over at my blog

Comments and Discussions

 
GeneralMy vote of 5 Pin
Vitaly Tomilov25-Jun-12 10:56
Vitaly Tomilov25-Jun-12 10:56 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.