Consuming Data from the Server Using RIA Services

In Chapter 4, we primarily focused on how you can expose data from the server using RIA Services. Let's now look at how you can consume this data in your Silverlight application. The RIA Services code generator has generated code in the Silverlight project that enables you to communicate with the domain services in your Web project. In this section, we'll look at this generated code, and how it helps us consume data from the server.

Inspecting the Generated Code in the Silverlight Project

We briefly discussed how the RIA Services code generator works in Chapter 4. Now that it's time to make use of it, let's inspect it a little closer. As you may recall, RIA Services automatically generates code in a hidden folder (named Generated_Code) under the Silverlight project that enables your Silverlight application to communicate with the domain services in your Web project. You can view the code generated in this folder (as shown in Figure 5-2) by selecting the Silverlight project in Solution Explorer and toggling the Show All Files button to “on,” using the second button from the left in the Solution Explorer window's toolbar.

images

Figure 5-2. The Silverlight project structure, showing the hidden Generated_Code folder

As you can see, there are a couple of files under the Generated_Code folder. There's a “core” generated class, named AdventureWorks.Web.g.cs (the selected file), containing most of the generated code, which we'll delve into further shortly. The rest of the files are the files in your Web project that you marked as being shared (i.e., gave a .shared.cs extension to). These shared files are simply copied from your Web project, and are organized in the same folder hierarchy that you have in your Web project.

images Note Inspecting the code generation files can also be useful when attempting to identify code generation problems. When debugging, you can also step into these files and set breakpoints to help identify issues. Just be sure not to modify these generated files, because any changes you make will be overwritten by the RIA Services code generator the next time it updates that file. If you want to add additional functionality to a generated class, create a separate file defining a partial class and extend the class in that manner.

Let's focus in on the AdventureWorks.Web.g.cs file. The name of this file is based upon the name of your Web project, using the format [webprojectname].g.cs. Because our Web project's name is AdventureWorks.Web, this file is named AdventureWorks.Web.g.cs. If you open this file, you will find that it consists of a number of classes. You should be aware of the following key classes:

  • Domain context classes
  • Model classes
  • The WebContext class

Let's look at each of these categories further.

images Note All classes in this file are partial classes, allowing you to extend them, if necessary, without modifying the generated code.

Domain Context Classes

The RIA Services code generator generates a domain context class for each domain service in the Web project. The code you write in your Silverlight project can make use of a domain context class to communicate its corresponding domain service on the server. In other words, a domain context class essentially acts as a proxy to facilitate communication with a domain service from the client.

Assuming you follow the standard naming convention for naming domain services (XXXService, where XXX can be anything of your own choosing), the RIA Services code generator will name the domain context XXXContext. For example, a domain service named ProductService in the Web project will have a corresponding domain context created in this file named ProductContext. If you keep to this naming scheme for your domain services, you should be able to work out the name of the corresponding domain context class very easily.

The domain context class also has methods corresponding to each query, invoke, and custom operation defined on the domain service. These methods allow you to call the corresponding methods on the domain service from the client.

We'll look at how you work with the domain context classes shortly.

images Note The insert/update/delete operations do not have corresponding methods created on the domain context, because they are never explicitly called from the client. Instead, as changes are made to an entity collection that's returned from a query operation, the domain context will maintain a changeset, consisting of the actions performed on the entities in the collection. When the SubmitChanges method is called on the domain context, RIA Services will send the changeset to the server and call the corresponding insert/update/delete operations on the domain service, as required.

Entity/Model Classes

Each entity (or presentation model class) exposed by a domain service will have a corresponding class created in this file. Any attributes applied to the class in the Web project (via its corresponding metadata class) will be applied directly to this generated client-side class.

The WebContext Class

The WebContext class is instantiated when your Silverlight application is started, and this instance is kept alive for the lifetime of the application as an “application extension service.” If you look at the code in the App.xaml.cs file in your AdventureWorks project, you'll find that an instance of this class is created when the application starts, and is added to the application's ApplicationLifetimeObjects collection. This keeps the instance alive as long as the application is alive. You can get a reference to this instance using the class's static Current property.

The purpose of this class is to act as the “context” for the application, maintaining the current user object, and providing access to an instance of the AuthenticationContext class. You can extend this class with a partial class if you'd like it to maintain any other data for you.

Customizing the Generated Code

As you've seen, RIA Services generates a lot of code for you. This code will suit most purposes, and the generated classes are extensible via partial classes, but sometimes you want more control over the code that is generated for you. You should never modify the generated code, as it will be overwritten the next time you compile your Web project. The RIA Services team has provided two methods to customize the generated code, however. You can replace the client proxy generator either with a T4 text template, or with a class that implements the IDomainServiceClientCodeGenerator interface. Your T4 template/client code generator class can then take the place of the standard RIA Services code generator, and generate the code itself.

This topic is beyond the scope of this book, but you can find more information on Varun Puranik's blog (http://varunpuranik.wordpress.com) or this blog post by Willem Meints: http://blogs.infosupport.com/using-t4-to-change-the-way-ria-services-work/

images Note You need the RIA Services Toolkit (discussed in Chapter 4) installed in order to have the tools to create a custom client code generator class/T4 template.

Consuming Data Declaratively in XAML via the DomainDataSource Control

RIA Services gives you the option of consuming data declaratively in XAML, where you can simply bind to the data via RIA Services' DomainDataSource control, or alternatively you can request the data in code using a domain context object. Let's start consuming data by looking at how the XAML-based method works.

The DomainDataSource Control

Declaratively wiring up your user interface to data from the server is a quick and easy way to consume data in your application. The key component that enables this XAML-based approach is the DomainDataSource control. The DomainDataSource control is a part of the RIA Services framework, and provides a bridge that enables you to declaratively interact with a domain context in XAML. You configure it by pointing it to a domain context, and telling it which method to call to request the data. The controls in your view can then bind to its Data property, from which the data retrieved from the server will be exposed. This method makes it very easy to consume data in your application without having to write any code.

Here is the XAML for a fairly standard use of a DomainDataSource control, which requests data from a domain service named ProductService:

<riaControls:DomainDataSource Name="productDomainDataSource"
                      AutoLoad="True"
                      QueryName="GetProductsQuery"
                      LoadedData="productDomainDataSource_LoadedData">
    <riaControls:DomainDataSource.DomainContext>
        <my:ProductContext />
    </riaControls:DomainDataSource.DomainContext>
</riaControls:DomainDataSource>

To support this XAML, a namespace prefix for the DomainDataSource control (riaControls) and for the ProductContext object (my) will also be declared in the root element of your XAML file, as follows:

xmlns:riaControls=
   "clr-namespace:System.Windows.Controls;images
                  assembly=System.Windows.Controls.DomainServices"
xmlns:my="clr-namespace:AdventureWorks.Web.Services;assembly=AdventureWorks"

Let's take a quick walkthrough of the important aspects of this XAML:

  • You need to set the Name property of the DomainDataSource control. The controls in your view that will display the data retrieved from the server will need to use ElementName binding to bind to this control (ElementName binding enables the property of one control to be bound to the property of another control). For those bindings to find this DomainDataSource control, it needs to have a name.

images Note ElementName binding is discussed in more detail in Chapter 11. You'll also see it in action in the following workshop.

  • The AutoLoad property is set to True, meaning that the call to the server will be made as soon as the view is loaded.
  • The QueryName property is set to the name of the method on the domain context that corresponds to the query domain operation that you want to call on the domain service. The operation on the domain service we want to call is named GetProducts, which has a corresponding method named GetProductsQuery on the domain context. We'll discuss this convention further shortly, when we discuss consuming data via code.
  • An event handler is assigned to handle the LoadedData event. The event handler contains code to display a message box if an error occurs while attempting to retrieve the data. It's not mandatory that you handle the LoadedData event, but it is recommended, as otherwise an exception will be thrown. This will be discussed further in the section “Handling Load Errors,” later in this chapter.
  • The DomainContext property is pointed to the domain context class that will handle obtaining the entity collection from the server. Because the domain service was named ProductService, the corresponding domain context in the Silverlight project will be named ProductContext.

These are the core requirements for configuring a DomainDataSource control, and after these properties are configured, your user interface controls can consume the data retrieved from the server by binding their DataContext or ItemsSource property to its Data property (using ElementName binding).

images Workshop: Querying Data in XAML

In this workshop, we'll consume data from the ProductService domain service that we created in Chapter 4, and display the results in a DataGrid control–all without writing any code. Although you could drag and drop a DomainDataSource control from the toolbox (or write the XAML by hand) and configure it manually, the easiest way to set up and configure one is to enlist the Data Sources window to help you. This is the method that we'll use in this workshop.

  1. Open the view named ProductList.xaml (created in Chapter 3), and delete any controls that you previously placed on the view.
  2. Open the Data Sources tool window (Data images Show Data Sources). Note that a data source has already been created for each domain context created by RIA Services in the Silverlight project, with the entities that they expose beneath them, as shown in Figure 5-3.
    images

    Figure 5-3. The Data Sources window

    images Note The icon next to the entity in the Data Sources window will indicate the type of control that will be created when the entity is dragged and dropped onto the design surface. You can change the type of control created by selecting the entity, clicking the drop-down button that appears (shown in Figure 5-3), and selecting an alternative control type or layout format from the menu.

  3. Drag the entity (in this case the Product entity) from the Data Sources window and drop it onto the design surface. This will create a DomainDataSource control for you, which is already configured to retrieve a collection of Product entities from the server. A DataGrid control has also been created, with its ItemsSource property bound to the Data property of the DomainDataSource control using ElementName binding.
    <riaControls:DomainDataSource AutoLoad="True"
                    d:DesignData="{d:DesignInstance my:Product, CreateList=true}"
                    LoadedData="productDomainDataSource_LoadedData"
                    Name="productDomainDataSource" QueryName="GetProductsQuery"
                    Height="0" Width="0">
        <riaControls:DomainDataSource.DomainContext>
            <my:ProductContext />
        </riaControls:DomainDataSource.DomainContext>
    </riaControls:DomainDataSource>

    <sdk:DataGrid AutoGenerateColumns="False" Height="200"
           HorizontalAlignment="Left"
           ItemsSource="{Binding ElementName=productDomainDataSource, Path=Data}"
           Margin="185,53,0,0" Name="productDataGrid" Width="400"
           RowDetailsVisibilityMode="VisibleWhenSelected" VerticalAlignment="Top">
        <sdk:DataGrid.Columns>
            <sdk:DataGridTextColumn x:Name="classColumn"
                                    Binding="{Binding Path=Class}"
                                    Header="Class" Width="SizeToHeader" />
            <sdk:DataGridTextColumn x:Name="colorColumn"
                                    Binding="{Binding Path=Color}"
                                    Header="Color" Width="SizeToHeader" />
            <!-- Additional columns removed for brevity-->
        </sdk:DataGrid.Columns>
    </sdk:DataGrid>

    images Note We didn't need to select in the Data Sources window the method that should be used on the domain context. Instead, the Data Sources window was able to infer the correct one, because usually there will be only one query domain operation on a domain service that returns the selected entity. However, if you do happen to have two or more query domain operations on a domain service that return the entity (or collection of entities) that you want to bind to, you can select which domain operation you want to be called by selecting the entity in the Data Sources window, clicking the drop-down button, and selecting the required domain operation from the menu.

  4. Run the application and navigate to the ProductListView.xaml view, using the menu button that you added for it in Chapter 3. The DataGrid control will be populated with all Product objects returned from the server.

Consuming Data in Code via the Domain Context Class

Whereas a XAML-based approach using the DomainDataSource control is a nice, easy way of populating a view with data from the server, often you will want to have more control over this process–especially if you're using the MVVM design pattern. Therefore, let's take a look at how you can request data from the server via code.

Mapping Domain Context Methods to Domain Service Operations

The query/invoke/custom operations on the domain service have corresponding methods on the domain context object that can be used to call them. These methods have the same name as the corresponding operation on the domain service, but are suffixed with Query. For example, the GetProducts operation on our ProductService domain service will have a corresponding method on the domain context named GetProductsQuery, as shown in Figure 5-4.

images

Figure 5-4. The domain service class and the corresponding domain context class, side-by-side

Requesting Data from the Server via Code

A number of steps are required when requesting data from the server via code. Calling the GetProductsQuery method on the domain context doesn't actually initiate the request to the server. Instead, it returns an EntityQuery object that represents the domain service query.

ProductContext context = new ProductContext();
EntityQuery<Product> qry = context.GetProductsQuery();

The concept of obtaining an EntityQuery object in order to query the server probably seems a little strange, but it makes more sense when you realize that sometimes you want to refine the query in order to have the data filtered/sorted/paged on the server before it is returned to the client. We will look at how you can do this in the “Manipulating Summary List's Contents” section of Chapter 6, but for now we'll just focus on requesting the full raw set of results from the database.

After you have an EntityQuery object, you can then pass it to the Load method on the domain context, which initiates the request for the data, like so:

LoadOperation<Product> operation = context.Load(qry);

Note that the Load method doesn't actually return you the data. Instead, it returns a LoadOperation object that contains an Entities property consisting of a collection of the requested objects; however, this collection will currently be empty. As with calling standard WCF Services in Silverlight, all calls to domain services using the RIA Services framework are asynchronous; therefore, the Load method will not wait for the data to be returned before returning control back to your code.

Now that the request for data has been made to the server, we need to wait for and then do something with the results. There are two ways you can go about this:

  • The LoadOperation object returned by the data context's Load method has a Completed event, which it raises when the data has been retrieved from the server. The event args parameter e passed into the event handler has an Entities property, from which you can access the results.
  • The LoadOperation object itself has an Entities property. This collection will initially be empty due to the asynchronous nature of the call, but will be automatically populated after the data is retrieved from the server. This allows you to immediately assign the collection to the DataContext or ItemsSource property of a control after the Load method has returned, and the control will update itself automatically when the collection is populated. This is possible because the collection implements the INotifyCollectionChanged interface, which contains an event named CollectionChanged. This event is used to notify listeners when items are added to or removed from the collection. Because Silverlight controls listen for the CollectionChanged event on collections that implement the INotifyCollectionChanged interface, they'll know to update themselves accordingly when the results have returned from the server, without the added complexity of handling the LoadOperation object's Completed event. We'll discuss this behavior further in Chapter 6 when we cover the ObservableCollection<T> type.

images Note The second method, using the Entities property on the LoadOperation object, is a much simpler and neater implementation for populating a ListBox or DataGrid control with results from the server. However, it is still recommended that you handle the LoadOperation object's Completed event, as per the first method, so you can identify whether an error occurred during the request, and handle it accordingly.

Putting all these steps together, we can display the results in a DataGrid control named productDataGrid with just the following code:

ProductContext context = new ProductContext();
EntityQuery<Product> qry = context.GetProductsQuery();
LoadOperation<Product> operation = context.Load(qry);
productDataGrid.ItemsSource = operation.Entities;

images Note This code will return all the products from the server, and display them in the DataGrid control. We'll look at how you can add filtering, sorting, and paging criteria shortly.

images Workshop: Querying Data in Code

In this workshop, we'll implement the steps described in the previous section, to populate a DataGrid control with Product entities from the server.

  1. Delete the DomainDataSource control and DataGrid control from the ProductListView.xaml view that you created in the previous workshop, so we essentially start with a blank view again.
  2. Add a DataGrid control to your view, and name it productDataGrid.
    <sdk:DataGrid Name="productDataGrid" />
  3. Open the code-behind file for the view (ProductListView.xaml.cs). Add the following using statements to the top of the class:
    using System.ServiceModel.DomainServices.Client;
    using AdventureWorks.Web;
    using AdventureWorks.Web.Services;
  4. In the constructor for the class, we now want to request data from the server, and assign the results to the DataGrid control's ItemsSource property as follows:
    public ProductListView()
    {
        InitializeComponent();

        ProductContext context = new ProductContext();
        EntityQuery<Product> qry = context.GetProductsQuery();
        LoadOperation<Product> loadOperation = context.Load(qry);
        productDataGrid.ItemsSource = loadOperation.Entities;
    }
  5. Run the application and navigate to the ProductListView.xaml view, using the menu button that you added for it in Chapter 3. The DataGrid control will be populated with all Product entities returned from the server.

images Note The preceding code does not handle any errors that could occur when making the request. We'll look at this shortly in the section “Handling Load Errors.”

Choosing the Right Approach

With two approaches available to choose from (XAML-based or code-based), you might be wondering how to decide which approach you should use. Data source controls provide a quick-and-easy means to populate your user interface with data. Being a declarative data-pull mechanism, a data source control doesn't require you to write any code. Also, as was demonstrated, the Data Sources tool window in Visual Studio makes configuring a view to display data from the server even easier. The XAML-based approach is often used in presentations for this very reason; however, in practical applications, this approach has its drawbacks.

The biggest concerns come from a design/architectural perspective. The use of data source controls leads to a tightly coupled application, where you are mixing the user interface definition and data access logic. Any form of tight coupling in software is generally considered bad practice, and something to be avoided in your design. Data source controls also lead to a loss of control over the process of retrieving/saving data, and also make any issues you have in communicating with the server difficult to debug.

The XAML-based approach is also not compatible with the MVVM design pattern popularly used in Silverlight applications (discussed further in Chapter 13). Therefore, if you are following this design pattern, using the DomainDataSource control isn't really applicable in any case.

In summary, the XAML-based approach is quick and easy–great for prototyping applications–but the code-based approach, especially when used in conjunction with the MVVM design pattern, leads to better application design.

Specifying Query Criteria

Passing additional clauses to the server to be appended to the database query is generally a messy process to implement with standard WCF Services. Often, you need to add additional parameters to the query operations to accept the clauses, and then manually append these clauses to the main query to be run before returning the results to the client.

RIA Services has a powerful solution to this problem. As you saw in Chapter 4, collections of entities can be exposed from a domain operation as an IQueryable<T>. When returning an IQueryable<T> expression (specifically a LINQ to Entities query), the power of the RIA Services framework is demonstrated in that you are able to write a LINQ query on the client specifying how you want the resulting entity collection filtered, sorted, and paged. RIA Services serializes this LINQ query and sends it to the server, where it is appended to the query expression returned from the query operation before it is executed. This enables the collection to be filtered/sorted/paged on the server without requiring the entire collection to be sent back to the client first, minimizing the required data traffic, and without the need for providing specific support for handling query criteria in the domain services.

images Note The ability to specify additional query clauses from the client does not allow the client to circumvent any filters you might have applied at the server on your query—for example, if you've applied a Where clause to filter out data that the user does not have permission to access. This feature does not open you up to SQL injection type attacks in any way.

From the client side, the DomainDataSource control contains the functionality to enable each of the data manipulation actions listed earlier to be applied to any data that it is configured to consume. You achieve this by defining descriptors on the DomainDataSource for these actions. You may define these actions either declaratively at design time (in XAML) or at runtime (in code). This makes performing those actions easy when using a XAML-based approach to consuming data.

images Note In Chapter 6, we'll look at how you can use the FilterDescriptors, SortDescriptors, and GroupDescriptors properties of the DomainDataSource control, along with the DataPager control to filter, sort, group, and page data in a declarative manner in XAML. For now, we'll focus on the code-based approach for applying criteria to a query operation on the client side using LINQ queries, when querying data from the server. Some familiarity with LINQ is assumed.

When using the code-based approach, you can add clauses to a query domain operation call using LINQ query operators. Let's take a look at what query operators are available for you to use, and how you can use these query operators when querying data from the server.

Client-Side Query Operators

In the previous workshop, “Querying Data in Code,” we simply requested the entire collection of entities that the domain operation could return. However, the EntityQueryable class provides you with a number of query operator extension methods. These methods enable you to append clauses to an EntityQuery object that will then be run on the server. The methods it provides include the following:

  • OrderBy: Orders the results, with the records sorted by the given property in ascending order.
  • OrderByDescending: Orders the results, with the records sorted by the given property in descending order.
  • Select: Does nothing; only empty selections are accepted.
  • Skip: Generally used when paging data to skip a given number of records before “taking” a given number.
  • Take: Generally used to limit the maximum number of records to be returned, or when paging data to return a given number of records after skipping a given number.
  • ThenBy: Adds an additional clause to order the records, with the records sorted by the given property in ascending order.
  • ThenByDescending: Adds an additional clause to order the records, with the records sorted by the given property in descending order.
  • Where: Used to filter the data being returned based upon given criteria.

images Note The EntityQueryable class does not provide a GroupBy method. Grouping in the context of returning data to display is actually a type of data sorting, so you would use the OrderBy and ThenBy methods instead to “group” the data.

images Workshop: Specifying Query Criteria Using Lambda Expressions

Following on from the previous workshop, “Querying Data in Code,” let's add a simple Where clause to the query to request only the out-of-stock products from the server.

  1. Add a using statement for the System.ServiceModel.DomainServices.Client namespace.
    using System.ServiceModel.DomainServices.Client;

    images Note Without the using statement, you won't see the query operator methods on the EntityQuery object.

  2. After you've retrieved the EntityQuery object from the domain context, apply the Where clause to it before passing it to the domain context's Load method, like so:
ProductContext context = new ProductContext();
EntityQuery<Product> qry = context.GetProductsQuery();
qry = qry.Where(p => p.SellStartDate <= DateTime.Now);
LoadOperation<Product> loadOperation = context.Load(qry);
productDataGrid.ItemsSource = loadOperation.Entities;

images Note Various combinations of these clauses can be appended one after the other in a fluent manner using the standard query operators and lambda expressions to achieve the results that you require. For example, you can both filter and order the results, like so:

qry = qry.Where(p => p.QuantityAvailable == 0).OrderBy(p => p.Name);

Now when you run this query from the client, the clauses you added will be automatically applied to the query on the server and included in the SQL query to the database that returns the results.

Specifying Query Criteria Using a Declarative Query Expression

LINQ provides two syntaxes for querying data: lambda expressions, as demonstrated in the previous workshop, and declarative query expressions. Some people prefer lambda expressions for their terseness, whereas others prefer the SQL-like declarative query expression syntax for its readability and similarity to SQL syntax. Throughout this book, we'll use lambda expressions in examples, but it's worth noting that you can also use declarative query expression syntax. Here is an example of using declarative query expression syntax to retrieve only the out-of-stock products from the server (as per the previous workshop):

ProductContext context = new ProductContext();
EntityQuery<Product> qry = from p in context.GetProductsQuery()
                           where p.SellStartDate <= DateTime.Now
                           select p;
LoadOperation<Product> loadOperation = context.Load(qry);
productDataGrid.ItemsSource = loadOperation.Entities;

images Note You can easily prove that your clauses have been applied to the database query on the server (and see the query that was generated) using SQL Profiler. Simply monitor SQL queries against your database and make a request for data (with additional clauses applied) from the client—the resulting SQL statement will be displayed in the profiler. If you don't have the full version of SQL Server, you can use a free tool with similar functionality to the SQL Profiler called SQL Server Express Profiler, created by AnjLab. You can download this tool from http://sites.google.com/site/sqlprofiler.

Explicitly Specifying a Domain Service Host

Note that we haven't had to explicitly specify the URI to the domain service when we consume data from the server. Fortunately, in the standard scenario, in which the site of origin for the Silverlight application also hosts the domain services, this is already taken care of for you. The domain context assumes that you will be communicating with a domain service from the site of origin (the web site that the Silverlight application is hosted on), and automatically determines the correct address of the domain service to communicate with. This means that you don't need to worry about manually reconfiguring the domain service addresses each time you deploy the application to a different host—the client will always look to the server from which it was obtained for the services.

However, it is possible to override this behavior by explicitly specifying an alternate URI to locate the services. If you have a scenario in which the application should communicate with domain services located on a different host from where your application was downloaded, you can specify the address of the corresponding domain service by passing in the new URI as a constructor parameter when instantiating the domain context, like so:

Uri uri =
    new Uri("http://localhost/AdventureWorks-Web-Services-ProductService.svc",
            UriKind.Absolute);
ProductContext context = new ProductContext(uri);

Note the format of the path to the service. Our ProductService domain service is located in the Services folder under the AdventureWorks.Web application. RIA Services generates a URI for each domain service in the project, enabling clients to use domain services as if they were standard WCF Services. You can also use this URI to point a domain context to a particular domain service. RIA Services doesn't actually create an actual .svc file, but it does listen for and handle requests for that URI. The URI will be located in the root of the application, and have a format of [ApplicationName]-[Folder]-[DomainServiceName].svc. Certain characters such as periods and slashes will be replaced by hyphens, as you'll note in the example.

images Note If you're having trouble determining the name of the .svc file, search for “[DomainServiceName].svc” (e.g., ProductService.svc) in the .g.cs file that the RIA Services code generator created for you under the hidden Generated_Code file in the Silverlight project. You will find its full name there.

Assuming that any cross-domain issues have been taken care of with a client access policy file on that server (discussed later in this chapter in the section “Cross-Domain Access Policies"), the domain context will communicate with the domain service at the specified URI instead.

images Note Unfortunately, specifying a domain service address is possible only with the code-based approach; this option is not available when using the DomainDataSource control.

Handling Load Errors

Because of the asynchronous nature of calls to the server in Silverlight, handling errors that are raised when a call to the server fails is a somewhat different process than you might be used to. For example, if you are using the code-based approach to consuming the data, you will find that putting try/catch blocks around a call to the server are of little use, as those calls immediately return control back to your code while the call happens on a background thread. Therefore, any errors that are raised on the server or in communicating with the server will not be caught by these exception handlers.

To actually catch these errors, you need to handle the event that is normally raised when the server communication is complete. When using the XAML-based approach, you will need to handle the DomainDataSource control's LoadedData event; when using the code-based approach, you will need to handle the LoadOperation's Completed event.

As mentioned earlier, if you drag and drop an entity from the Data Sources window onto the design surface, the DomainDataSource control that is created will already handle the LoadedData event and check for whether an exception occurred, as follows:

private void productDomainDataSource_LoadedData(object sender,
                                                LoadedDataEventArgs e)
{
    if (e.HasError)
    {
        System.Windows.MessageBox.Show(e.Error.ToString(), "Load Error",
                                       System.Windows.MessageBoxButton.OK);
        e.MarkErrorAsHandled();
    }
}

images Note Simply dropping a DomainDataSource control from the toolbox will not automatically create an event handler to handle its LoadedData event. Using the method of dropping an entity from the Data Sources window onto the design surface is the only way to do so.

If you are using the code-based approach, handling the LoadOperation's Completed event is very similar to handling the DomainDataSource's LoadedData event, except you need to cast the sender parameter to a generic LoadOperation object, like so:

private void loadOperation_Completed(object sender, EventArgs e)
{
    LoadOperation<Product> op = sender as LoadOperation<Product>;

    if (op.HasError)
    {
        System.Windows.MessageBox.Show(op.Error.ToString(), "Load Error",
                                       System.Windows.MessageBoxButton.OK);
        op.MarkErrorAsHandled();
    }
}

Note how we are calling the MarkErrorAsHandled method in both the DomainDataSource control's LoadedData event handler and the LoadOperation object's Completed event handler. If you don't call this method, the domain context will throw the exception, which will become an unhandled exception. Unhandled exceptions are caught by the Application_UnhandledException event handler method in the App class and handled there, showing a pop-up error window displaying the details of the error, to prevent your application from crashing. However, best practice is to prevent exceptions reaching this event handler where possible.

You can check the exception to find out what type of exception occurred and handle it accordingly. The exception is returned simply as an Exception type, but you can cast it to a DomainOperationException to get more useful information about it. Of most interest is the Status property that it exposes. This will tell you what type of exception occurred, using the OperationErrorStatus enumeration, so that you can handle it accordingly. The following values in this enumeration are of most interest when loading data:

  • ServerError: An exception was raised on the server, or the application could not reach the server.
  • Unauthorized: The user does not have permission to perform the operation. Restricting access to domain operations based upon the user's role is discussed in Chapter 8.

Therefore, you can check this value to determine the category of the error, and handle it accordingly. The following example demonstrates a structure for handling the errors in the LoadedData event handler of the DomainDataSource control. (Change e to the instance of the LoadOperation if using the domain context method.)

if (e.HasError)
{
    DomainOperationException error = e.Error as DomainOperationException;

    switch (error.Status)
    {
        case OperationErrorStatus.ServerError:
            // Handle server errors
            break;
        case OperationErrorStatus.Unauthorized:
            // Handle unauthorized domain operation access
            break;
    }
}

images Note If you are using the code-based approach but don't want an exception to be thrown when an error occurs, use one of the overloads of the domain context's Load method that accepts the throwOnError parameter, and set it to false.

You can test your exception handler by throwing an exception in your query domain operation on the domain context. For example, add the following code to the domain operation method, before returning the query results, to see the effect of that exception in your Silverlight application:

throw new Exception("Test exception handling exception!");
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset