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.
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.
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.
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:
WebContext
classLet's look at each of these categories further.
Note All classes in this file are partial classes, allowing you to extend them, if necessary, without modifying the generated code.
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.
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.
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 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.
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/
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.
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.
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;
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:
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. Note ElementName
binding is discussed in more detail in Chapter 11. You'll also see it in action in the following workshop.
AutoLoad
property is set to True
, meaning that the call to the server will be made as soon as the view is loaded.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.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.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).
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.
ProductList.xaml
(created in Chapter 3), and delete any controls that you previously placed on the view.Figure 5-3. The Data Sources window
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.
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>
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.
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.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.
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.
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:
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.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. 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;
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.
In this workshop, we'll implement the steps described in the previous section, to populate a DataGrid
control with Product
entities from the server.
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.DataGrid
control to your view, and name it productDataGrid
.
<sdk:DataGrid Name="productDataGrid" />
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;
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;
}
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.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.”
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.
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.
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.
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.
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. 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.
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.
using
statement for the System.ServiceModel.DomainServices.Client
namespace.
using System.ServiceModel.DomainServices.Client;
Note Without the using
statement, you won't see the query operator methods on the EntityQuery
object.
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;
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.
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;
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
.
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.
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.
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.
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();
}
}
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;
}
}
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!");