Accessing Master-Detail Data through ADO.NET Data Service in a Silverlight Application (Part 1)

With Visual Studio 2010 Beta2, users can add data sources to a Silverlight application using the Data Source Configuration Wizard. The added data source is shown in the Data Sources tool window, and can be drag-dropped to Silverlight designer, with data-bound controls automatically generated.

In this post, I’ll walk you through the steps of creating an ADO.NET Data Service to access data using Entity Data Model, and creating a Silverlight application to consume the data service. We’ll build a classic Northwind order management application where users can do CRUD operations on customer and order information through the service. The completed application will look like this:
image

Note: Although the following walkthrough uses Visual C# as the programming language, you can complete it in VB as well to achieve the same functionalities.

Creating an ADO.NET Data Service
To create an ADO.NET Data Service, we will need to first create a new ASP.NET Web Application to host the service. Here are the steps to create the project, define an ADO.NET Entity Data Model, and add ADO.NET Data Service:

1. Select Visual Studio File->New Project menu, and choose ASP.NET Web Application.

2. From Solution Explorer, right click the Web Application project node, and choose Add New Item. In the Add New Item dialog, choose ADO.NET Entity Data Model under the Data category. This will invoke the Entity Data Model Wizard.

3. Follow the Entity Data Model Wizard to connect to a Northwind SQL Server database, and choose tables: Customers, Orders, and Order_Details.

4. From Solution Explorer, right click the Web Application project node, and choose Add New Item. In the Add New Item dialog, choose ADO.NET Data Service under the Web category.

5. The WebDataService1.svc.cs (or WebDataService1.svc.vb) file will be opened in VS code editor. We’ll need to modify the code as follows to expose the data model and set appropriate access rules. Note we have changed the default EntitySetRights from AllRead to All so that we are able to update the entities later.

public class NorthwindWebDataService : DataService< NorthwindEntities >

{

public static void InitializeService(DataServiceConfiguration config)

{

config.SetEntitySetAccessRule("*", EntitySetRights.All);

config.SetServiceOperationAccessRule("*", ServiceOperationRights.All);

config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;

}

}

6. Build the project.

At this point, our Data Service is ready to be consumed by client applications.

Creating a Silverlight Application to Consume the ADO.NET Data Service
Now let’s create a Silverlight Application to consume the data service. The following steps create a Silverlight application, and add a new data source to consume the above ADO.NET Data Service:

1. From Visual Studio File->Add New Project menu, choose Silverlight Application.

2. On the New Silverlight Application dialog, accept the default to host the Silverlight Application in the above ASP.NET Web Application.

3. From Visual Studio Data menu, choose Add New Data Source.

4. On the Data Source Configuration Wizard, choose Service as the data source type, and click Next.

5. This will bring up the Add Service Reference dialog. Since our data service is in the same solution, we can click the Discover button to find the above ADO.NET Data Service, or type in the service address (like https://localhost:<PORT NUMBER>/WebDataService1.svc), and click OK.

6. Finish the Data Source Configuration Wizard. A new Service Reference will be added in Solution Explorer, with a code file containing data classes used to access and interact with data service resources as objects.

Drag-dropping Data Sources onto Silverlight Designer
In the above steps, we have added a Service data source to the Silverlight application. If you open the Data Sources tool window, the data objects accessible through the ADO.NET Data Service should be shown and available for drag-dropping to the Silverlight designer. However, due to a bug not yet fixed, you may need to close VS and then reopen it (this bug applies only to data services created within the same solution. If you have an existing data service, the Data Sources window will show the data objects immediately).

1. From Visual Studio Data menu, choose Show Data Sources. The Data Sources tool window will show up with data objects from the ADO.NET Data Service: Customers, Orders, and Order_Details.

2. In the Northwind Order Manager application, we want to show the customer information as details rather than the default DataGrid. To do this, in the Data Sources window, drop-down the Customers ComboBox, and choose Details.

3. Now drag Customers node to MainPage.xaml designer. The customer information is generated on the designer as data-bound controls. The XAML code also contains data binding information similar to the following:
<CollectionViewSource x:Key="customersViewSource" d:DesignSource="{d:DesignInstance my:Customer, CreateList=True}" />

4. To show the related Orders and Order_Details information, drag Customers.Orders (IMPORTANT: this is the child node Orders, under Customers node) and Customers.Orders.Order_Details from Data Sources Window to MainPage.xaml designer. The generated CollectionViewSource nodes in the XAML file will be similar to the following:
<CollectionViewSource x:Key="customersOrdersViewSource" Source="{Binding Path=Orders, Source={StaticResource customersViewSource}}" />

<CollectionViewSource x:Key="customersOrdersOrder_DetailsViewSource" Source="{Binding Path=Order_Details, Source={StaticResource customersOrdersViewSource}}" />

Writing Code to Load Data at Run Time
We’ll need to write some code to load the data at run time. Let’s declare some private members in the MainPage class in MainPage.xaml.cs:

        private NorthwindEntities ctx; // The data context

        private CollectionViewSource cvs; // The CollectionViewSource to bind data 

        private DataServiceQuery<Customer> qry; // The query to load data

        private System.Collections.ObjectModel.ObservableCollection<Customer> data = new System.Collections.ObjectModel.ObservableCollection<Customer>();

 

In the MainPage.xaml.cs LayoutRoot_Loaded event handler, add the following code to instantiate the data service, define a DataServiceQuery and execute it to retrieve data. The retrieval of data is done through an asynchronous call.

        private void LayoutRoot_Loaded(object sender, RoutedEventArgs e)

        {

            ctx = new NorthwindEntities(new Uri("https://localhost:<PORT NUMBER>/NorthwindWebDataService.svc"));

            cvs = this.Resources["customersViewSource"] as CollectionViewSource;

            qry = ctx.Customers.Expand("Orders").Expand("Orders/Order_Details");

            qry.BeginExecute(new AsyncCallback(OnDataLoadComplete), null);

        }

 

        private void OnDataLoadComplete(IAsyncResult result)

        {

            List<Customer> custs = qry.EndExecute(result).ToList();

            foreach (Customer cust in custs)

            {

                data.Add(cust);

            }

            cvs.Source = data;
        }

Note we are using an ObservableCollection<T> to be the Source of the CollectionViewSource, rather than just a List<T> or DataServiceQuery<T> . This is so that we can get notifications when data get added, updated or removed since we are going to update and save data later. If you do not need to update data, you may just need to set the CollectionViewSource.Source to the DataServiceQuery like :

cvs.Source = ctx.Customers.Expand("Orders").Expand("Orders/Order_Details");

Navigating through Data
To add the functionality of navigating through data at run time, we can create a Silverlight User Control similar to the following:

image

The XAML for one of the buttons is similar to this:

<Button Margin="5" IsEnabled="False" Name="moveFirstItem" Click="moveFirstItem_Click"> 
    <Image Height="23" Name="moveFirstItemImage" Source="BindingNavigator_Icons/MoveFirstItem.ico">

        <Image.OpacityMask>

            <ImageBrush>

                <ImageBrush.ImageSource>

                    <BitmapImage UriSource="BindingNavigator_Icons/MoveFirstItem.ico" />

                </ImageBrush.ImageSource>

            </ImageBrush>

        </Image.OpacityMask>

    </Image>

</Button>

 

To move to the beginning of the data collection, we can handle the MoveFirstItem button Click event with the following code:

private void moveFirstItem_Click(object sender, RoutedEventArgs e)

{

    cvs.View.MoveCurrentToFirst();

    RefreshControls();

}

 

The RefreshControls() method handles the logic to update the current position display and enable or disable buttons based on the current record position:

private void RefreshControls()

{

    int pos = cvs.View.CurrentPosition;

    this.positionItem.Text = (pos + 1).ToString(System.Globalization.CultureInfo.CurrentCulture);

    this.countItem.Text = String.Format(this.countItemTemplate, this.recordCount);

    this.moveFirstItem.IsEnabled = (pos > 0);

    this.movePreviousItem.IsEnabled = (pos > 0);

    this.moveNextItem.IsEnabled = (pos < this.RecordCount - 1);

    this.moveLastItem.IsEnabled = (pos < this.RecordCount - 1);

    this.deleteItem.IsEnabled = (this.RecordCount > 0);

}

 

Similarly, we can use the following code to navigate through the records by moving back and forth:

private void movePreviousItem_Click(object sender, RoutedEventArgs e)

{

    cvs.View.MoveCurrentToPrevious();

    RefreshControls();

}

private void moveNextItem_Click(object sender, RoutedEventArgs e)
{
    cvs.View.MoveCurrentToNext();
    RefreshControls();
}

private void moveLastItem_Click(object sender, RoutedEventArgs e)
{
    cvs.View.MoveCurrentToLast();
    RefreshControls();
}

We also provide the functionality to navigate to a specific position using the TextBox to accept user input, and navigate when the user presses the ENTER key with a valid position entered. If the number entered is not valid, it will just be ignored and the record stays at the current position. If the number is smaller than 1, we navigate to the first record instead. If the number entered is greater than the record count, we navigate to the last record.

private void positionItem_KeyDown(object sender, KeyEventArgs e)

{

    if (e.Key == Key.Enter)

    {

        TextBox txt = sender as TextBox;

        int curPos = this.cvs.View.CurrentPosition + 1;

        int pos;

        bool isValid = int.TryParse(txt.Text, out pos);

        if (!isValid)

        {

            txt.Text = curPos.ToString();

        }

        else

        {

            if (pos <= 1)

            {

                pos = 1;

            }

            else if (pos > this.recordCount)

            {

                pos = this.recordCount;

            }

            if (curPos != pos)

            {

                this.cvs.View.MoveCurrentToPosition(pos - 1);

            }

            txt.Text = pos.ToString();

            RefreshControls();

        }

    }

}

Next Steps

The next part of this post will add functionalities to update and save Customers, Orders and Order_Details. We will also develop the application to be able to delete data and add new records.