All client / server communications in Silverlight must be asynchronous and desktop applications probably should be. Asynchronous programming requires some care and techniques that may be unfamiliar. This topic describes the asynchronous operations in DevForce and how to use them.
The DevForce EntityManager in the full .NET library sports numerous methods for communicating from the client to the server, split 50/50 between synchronous and asynchronous versions.
When using the synchronous versions, the client code makes a call and waits for the server to return.
C# | results = manager.ExecuteQuery(someQuery); doSomething(results); // called after server returns |
VB | results = manager.ExecuteQuery(someQuery)() doSomething(results) ' called after server returns |
The application on the calling thread blocks until the server returns its reply. If it takes awhile, the calling code waits. If the calling code is UI code, the user will be staring at a frozen screen until the queried entities arrive.
It could be a long wait. The asynchronous operations might be a better choice. They don't deliver server results any faster. They do let you keep the UI alive. The user might be able to do other work while waiting. Even if they can't, the application can show that it is still running and perhaps tell the user how matters are progressing.
Many synchronous methods aren't even available in the Silverlight EntityManager, since Silverlight disallows synchronous communications with the server. Only queries which may be fulfilled from cache can be executed synchronously in Silverlight. All service methods are asynchronous; they include:
You have to take a different approach when using these methods. The synchronous code we wrote earlier doesn't translate literally to an asynchronous alternative. For example, this won't do what you're expecting:
C# | // Not what you are expecting results = manager.ExecuteQueryAsync(someQuery); doSomething(results); // called immediately, before results arrive |
VB | ' Not what you are expecting results = manager.ExecuteQueryAsync(someQuery)() doSomething(results) ' called immediately, before results arrive |
Two critical observations:
It is in the nature of any asynchronous method call that control flows immediately to the next line and you cannot predict when the server will return with results. If you care when the results arrive, you must provide some means for the asynchronous operation to notify you.
DevForce asynchronous methods can notify you in one of two ways:
C# | // with callback manager.ExecuteQueryAsync(query1, queryCompletedCallback); // with CompletedEvent operation = manager.ExecuteQueryAsync(query2); operation.Completed += queryCompletedHandler; |
VB | ' with callback manager.ExecuteQueryAsync(query1, queryCompletedCallback) ' with CompletedEvent operation = manager.ExecuteQueryAsync(query2) AddHandler operation.Completed, AddressOf queryCompletedHandler |
All DevForce asynchronous methods support these two approaches.
The ExecuteQueryAsync signature tells a more complete story:
C# | public EntityQueryOperation<T> ExecuteQueryAsync<T>( IEntityQuery<T> query, Action<EntityQueryOperation<T>> userCallback = null, Object userState = null) |
VB | Public Function ExecuteQueryAsync(Of T)( ByVal query As IEntityQuery(Of T), _ ByVal Optional userCallback As Action(Of EntityQueryOperation(Of T)) = Nothing, _ ByVal Optional userState As Object = Nothing _ As EntityQueryOperation(Of T) |
Let's break this signature down starting with the method's returned value and proceeding through the parameters.
All asynchronous methods return an "operation coordinator" object ending in the word "Operation". In this example, it is an EntityQueryOperation with a type parameter 'T' specifying the type of the query result. 'T' would be Customer if the query returned Customer entities.
The other asynchronous methods have their own operation object types such as the EntitySaveOperation for a save. They all derive from the DevForce BaseOperation class and therefore share many of the same behaviors and members such as a Completed event.
The operation object is not the operation itself. It is an object that represents the operation in progress. You can ask it questions such as "are you finished?" and "do you have errors?". You can tell it to cancel the operation. It is a vehicle for coordinating the caller and the operation. Most developers just call it "operation" for the sake of brevity.
Most asynchronous methods take a functional parameter such as the query. This is the same parameter that would be passed into the corresponding synchronous method.
Next is the optional callback method. The callback is an Action that takes the operation coordination object as its sole parameter. This is the same object that was returned by the method, the EntityQueryOperation<T> object in our example.
The last parameter is an optional userState. The userState is an arbitrary object of your choosing. You supply a userState when you need to pass some information to the callback method or Completed event handler. You might use it to distinguish one query from another. The userState object can be as simple as an integer or it can be an arbitrarily complex custom type. DevForce assigns a Guid if you don't specify a value.
An asynchronous operation callback method takes an operation coordination object parameter as seen in this example.
C# | void AllCustomersQuery() { var query = new EntityQuery<Customer>(); manager.ExecuteQueryAsync(query, CustomerQueryCallback); } void CustomerQueryCallback(EntityQueryOperation<Customer> op) { if (op.CompletedSuccessfully) { var resultList = op.Results; Console.WriteLine("Query returned {0} entities", resultList.Count()); } else { /* do something with op.Error */ } } |
VB | Sub AllCustomersQuery() Dim query = New EntityQuery(Of Customer) manager.ExecuteQueryAsync(query, AddressOf CustomerQueryCallback) End Sub Sub CustomerQueryCallback(ByVal op As EntityQueryOperation(Of Customer)) If (op.CompletedSuccessfully) Then Dim resultList = op.Results Console.WriteLine("Query returned {0} entities", resultList.Count()) Else ' do something with op.Error End If End Sub |
Many developers prefer to use a lambda callback for economy of expression:
C# | void AllCustomersQuery() { var query = new EntityQuery<Customer>(); manager.ExecuteQueryAsync(query, op => { if (op.CompletedSuccessfully) { var resultList = op.Results; Console.WriteLine("Query returned {0} entities", resultList.Count()); } else { /* do something with op.Error */ } }); } |
VB | Sub AllCustomersQuery() Dim query = New EntityQuery(Of Customer) manager.ExecuteQueryAsync(query, Sub(op) If (op.CompletedSuccessfully) Then Dim resultList = op.Results Console.WriteLine("Query returned {0} entities", resultList.Count()) Else ' do something with op.Error End If End Sub) End Sub |
Instead of a callback you may prefer to listen to the Completed event. In the next code sample, we simplify the query and call its ExecuteAsync method instead of the EntityManager's ExecuteQueryAsync. The principles and practices are the same as before.
C# | void AllCustomersQuery() { var op = manager.Customers.ExecuteAsync(); op.Completed += CustomerQueryCompletedHandler; } void CustomerQueryCompletedHandler( object sender, EntityQueriedEventArgs<Customer> args) { if (args.CompletedSuccessfully) { var resultList = args.Results; Console.WriteLine("Query returned {0} entities", resultList.Count()); } else { /* do something with args.Error */ } } |
VB | Sub AllCustomersQuery() Dim op = manager.Customers.ExecuteAsync() AddHandler op.Completed, AddressOf CustomerQueryCompletedHandler End Sub Sub CustomerQueryCompletedHandler( _ ByVal sender as Object, ByVal args As EntityQueryOperation(Of Customer)) If (args .CompletedSuccessfully) Then Dim resultList = args .Results Console.WriteLine("Query returned {0} entities", resultList.Count()) Else ' do something with args .Error End If End Sub |
The handler takes an EventArgs instead of an operation coordination object but the effect is the same.
You can use a lambda expression here too:
C# | void AllCustomersQuery() { var op = manager.Customers.ExecuteAsync(); op.Completed += (o, args) => { if (args.CompletedSuccessfully) { var resultList = args.Results; Console.WriteLine("Query returned {0} entities", resultList.Count()); } else { /* do something with args.Error */ } }; } |
VB | Sub AllCustomersQuery() Dim op = Manager.Customers.ExecuteAsync() AddHandler op.Completed, Sub(o As Object, args As EntityQueriedEventArgs(Of Customer)) If args.CompletedSuccessfully Then Dim resultList = args.Results Console.WriteLine("Query returned {0} entities", resultList.Count()) Else ' do something with args.Error End If End Sub End Sub |
The operation coordination object and the EventArgs have additional members. These differ depending upon the operation performed; consult the API documentation for details. BaseOperation reveals the members in common.
Member | Description |
---|---|
CanCancel | Get if this operation is cancellable. |
Cancel() | Try to cancel the operation. It may (still) be cancellable. |
Cancelled | Get if the operation was canceled before it completed. |
Completed | Raised when the operation has completed, successfully or otherwise. A synonym for IsCompleted. |
CompletedSuccessfully | Get if the operation completed without error and without having been cancelled. |
CompletedSynchronously | Get if the operation was actually processed synchronously entirely on the caller's thread without a trip to the server. This happens frequently with query operations that are completely satisfied by cached entities. |
Error | If the operation failed, this property returns the error as an Exception. Returns null if there is no error (yet). |
HasError | Get if the operation failed with an error. |
IsCompleted | Get if the operation finished, successfully or otherwise. A synonym for Completed. |
IsErrorHandled | Get or set whether the error was handled. Error handling logic must set this to true; you usually call the MarkErrorAsHandled method. If the operation failed and this property is not true, DevForce re-throws the error, an exception that you cannot catch is likely to terminate the application. See error handling for details. |
MarkErrorAsHandled() | Sets the IsErrorHandled property to true. |
PropertyChanged | Raised when one of the operation's public properties has changed. |
UserState | Get the custom object that carries information from the caller to the callback. |
We mentioned above that you can't wrap your asynchronous method in a try/catch. But you must still be on the lookout for errors, since any unhandled exception from an asynchronous operation will terminate the application.
The exception raised by the asynchronous operation is returned in the Error property of the operation or EventArgs object. These expose the following properties useful in error detection and handling:
It's your responsibility to inspect the operation object when the async operation completes and address failures.
Mark the error "as handled"
If an async operation completes with an error and you don't either call MarkErrorAsHandled or set IsErrorHandled to true, then DevForce will re-throw the exception. You won't be able to catch this re-thrown exception, and your application will crash.
If you don't want the unhandled exception to terminate your application, you must:
Here's our query completed handler from earlier with a bit more error handling:
C# | void CustomerQueryCompletedHandler(object sender, EntityQueriedEventArgs<Customer> args) if (args.CompletedSuccessfully) { var resultList = args.Results; Console.WriteLine("Query returned {0} entities", resultList.Count()); } else if (args.HasError) { HandleError(args.Error); // Your choice - log the error, show a message, ... args.MarkErrorAsHandled(); } } |
VB | Sub CustomerQueryCompletedHandler( _ ByVal sender as Object, ByVal args As EntityQueryOperation(Of Customer)) If args.CompletedSuccessfully Then Dim resultList = args.Results Console.WriteLine("Query returned {0} entities", resultList.Count()) ElseIf args.HasError Then HandleError(args.Error) ' Your choice - log the error, show a message, ... args.MarkErrorAsHandled() End If End Sub |
The EntityManager also supports the IAsyncResult asynchronous pattern through an explicit implementation of the IEntityManagerAsync interface . You will need to cast an EntityManager to this interface in order to use methods that follow this pattern.
In the IAsyncResult pattern, an asynchronous operation named "SomeOperation" is implemented as two methods named BeginSomeOperation and EndSomeOperation.