Is it just me or is this article’s title a little ridiculous? I think anytime you have a name that makes use of a colon, a hyphen, and parentheses, that’s a good sign you’ve gone too far. I was really tempted to throw an exclamation point at the end, but I was afraid that would cause this entire blog to implode on itself. But you know what? That’s just the kind of grammatical insanity you can expect from this site.
In the previous article we saw the Sys.Data.DataService class’s insert, update, and remove methods, which beautifully allow us to perform data modification operations against a data service using the ASP.NET AJAX libraries. We also learned that as powerful as that method trifecta is, it lacks a unit of work behavior that is sometimes needed, and instead makes individual service requests for each call. I made the statement that the DataService class doesn’t support the unit of work pattern, and that statement is in fact true. But, that doesn’t mean that you can’t achieve a unit of work by other means. There are two classes in the ADO.NET Data Services AJAX libraries that we haven’t yet covered that offer the ability to group together data modification operations into a batch, and execute them all at once. These two classes are Sys.Data.ActionSequence, and Sys.Data.ActionResult.
An ActionSequence object is simply a wrapper on top of a DataService. It maintains a list of data operations, and then uses its underlying DataService reference to execute the operations one by one. There are two ways to create an ActionSequence:
- Using its constructor, which takes a DataService instance
- Calling the DataService class’s createActionSequence method
Of the two choices, the later would be the preferred method (by me at least). You’re already going to have a reference to a DataService object, so once you determine that you need to create an operation batch, you can simply generate an ActionSequence object for your respective DataService like so:
var sequence = service.createActionSequence();
Now instead of using the DataService reference directly, by calling its insert/update/remove methods, I’ll work with the newly created ActionSequence instance to build my operations batch. At any time when working with an ActionSequence, if you need a reference to its internal DataService, you can use its service property.
var service = sequence.get_service();
That way you can pass around a reference to an ActionSequence and still be able to access its underlying DataService if needed. This property is read-only, so you’re stuck with the DataService used when the ActionSequence was created.
The ActionSequence class has methods that mirror the insert/update/remove methods of the DataService class, with a few differences:
- The ActionSequence methods don’t return a Sys.Net.WebRequest instance. This is because as opposed to the DataService, each individual operation isn’t served by a specific WebRequest.
- The ActionSequence methods don’t take success/failure callbacks as parameters. This is because you aren’t concerned with responding to the success or failure of the individual operations, but rather the entire batch.
- The ActionSequence methods don’t execute immediately.
Note that throughout the rest of the article, the variable “sequence” will simply refer to the ActionSequence instance created above.
Inserting
To add an insert operation to the ActionSequence, you simply call its addInsertAction method. This method takes 4 parameters: the entity object to insert, the name of the entity set to insert it into, arbitrary context data, and a boolean that determines whether you want to pass the refreshed entity object data back to the sequence’s execution callback (the default value is true). Only the first two parameters are required, and the last two are optional, and familiar from the DataService’s insert method.
var manufacturer = { Name: "JC's Awesome Phones" }; sequence.addInsertAction(manufacturer, "Manufacturers");
As you can see, nothing special is going on here. The logic to create a new entity object hasn’t changed, and we’re working with our ActionSequence pretty similarly to the way we did with our DataService in the last article. If you don’t need context data, and you want the ActionSequence to pass back your refreshed entity data, your call to addInsertAction can be as simple as seen above.
Our ActionSequence now contains a single command to insert the new manufacturer upon submission. No action has been taken yet, we’re simply building up our batch.
Updating
In order to add an update operation to the ActionSequence, you call its addUpdateAction method. This method takes the exact same parameters as the addInsertAction method. The only differences are:
- The URI parameter isn’t required.
- The URI parameter refers to a specific entity resource, not an entity set.
- The fourth boolean parameter defaults to false instead of true.
I’m going to query for a specific manufacturer:
sequence.get_service().query("Manufacturers?$filter=Name eq 'Nokia'", onSuccess);
Note how I’m accessing the underlying DataService from my ActionSequence reference. This is great because you only have to maintain a reference to your sequence, and not also to your DataService.
I’m now going to change their name, and add that modification to the ongoing sequence:
function onSuccess(result) { result[0].Name = "Nokia is cool"; sequence.addUpdateAction(result[0]); }
Note that because I don’t have any context data, and don’t care about the data being passed back to the sequence’s callback, I can simply call addUpdateAction, passing it the modified entity object instance, and nothing else. For the above to work, my ActionSequence instance obviously has to be global. If you don’t prefer to go that route, I could just as easily have passed the ActionSequence reference around as my context data:
sequence.get_service().query("Manufacturers?$filter=Name eq 'Nokia'", onSuccess, null, sequence);
And modified my success callback like so:
function onSuccess(result, context) { result[0].Name = "Nokia is cool"; context.addUpdateAction(result[0]); }
You can begin to see the uses of the context data parameter. You can get pretty creative with it and do some pretty cool stuff. The only down side to the above approach is that you might loose some explicitness in your code. The above success callback doesn’t make it clear that the context parameter is an ongoing ActionSequence object. You could obviously rename the parameter to something like “sequence” instead, which would help, but still might not be as clear as you want. If you want to get really cryptic you could even do this all in one step:
sequence.get_service().query("Manufacturers?$filter=Name eq 'Nokia'", function(result, context) { result[0].Name = "Nokia is cool"; context.addUpdateAction(result[0]); }, null, sequence);
However you decide to approach this, you can see the flexibility you have, which is always a good thing.
Deleting
If you want to add a delete operation, you simply call the ActionSequence’s addRemoveAction method. This method takes three parameters: the entity object instance to delete, the canonical URI of the entity instance to delete, and arbitrary context data. All three parameters are optional. Just like the DataService’s remove method, addRemoveAction allows you to either specify just the entity object reference, just the entity’s URI, or both. All three scenarios will successfully delete data. I prefer the URI approach, since it saves you a query to the data service.
sequence.addRemoveAction(null, "Manufacturers(2)");
If I already had a reference to the manufacturer instance I wanted to delete (or I reconstructed it), I could just call:
sequence.addRemoveAction(manufacturer);
Use whichever form works best for your situation.
Alright, so I’ve now got an ActionSequence that contains three operations:
- I’m inserting a new manufacturer called “JC’s Awesome Phones”.
- I’m renaming the Nokia manufacturer to “Nokia is cool”.
- I’m deleting the manufacturer with the Id of 2 (sorry #2)
Now that I’ve got this batch, how do I actually run it? You simply call the ActionSequence’s executeActions method. It takes two parameters: a callback, and arbitrary context data. Note I didn’t call it a “success callback”, and there is no failure callback. This is because you are executing a batch of operations, some of which may fail, and some of which may succeed. The callback you specify to the executeActions method is simply going to be called when all sequence operations have completed, regardless of their success or failure. Note also that the context data provided to the executeActions method is representative of the entire batch. As you recall, each individual operation has their own context data as well (if specified), so only provide context data to the executeActions method if you want some arbitrary data scoped to the entire group.
The signature for the callback method provided to the executeActions method takes three paramaters: an array of Sys.Data.ActionResult objects, a boolean that determines whether any of the sequence’s operations failed, and the context data provided to the call to the executeActions method (if any). The later two parameters are optional, so if you don’t care about context data, or checking if any errors occurred at the batch level, you can simply omit them.
I’m not concerned with any context data at this point, so I’ll just execute my sequence, passing it a callback:
sequence.executeActions(sequenceCompleted);
Since I’m not using any context data, my callback method could look like the following:
function sequenceCompleted(results, hasError) { }
The hasError parameter will be true if an error occurred in any of the sequence’s operations. This isn’t necessarily useful, since you don’t know which operation failed, but it can be used to make a high-level check, before you do any further digging for error resolution. The real point of interest here is the results parameter, which holds an array of Sys.Data.ActionResult instances. The ActionResult class represents the result of an individual data modification operation that was a part of an ActionSequence. So for every operation you add to the sequence, you will have an ActionResult object. In our case, the results parameter will be an array of three ActionResult objects.
The ActionResult class contains three read-only properties and no methods:
- actionContext
- operation
- result
The actionContext property simply returns whatever arbitrary context data you specified (if any) when you called addInsertAction, addUpdateAction, or addRemoveAction. Between the individual context data for each ActionResult, and the context data for the overall batch, you can get pretty creative
The operation property returns a string that equals either: “insert”, “update”, or “remove”, depending on what the actual operation was.
The result property is the most interesting, as its value depends on the actual operation:
- If the operation was an insert or update, and you specified to pass back the refreshed entity data when adding the operation to the sequence, then the result property will return a refreshed version of the inserted/updated entity object.
- If the operation was an insert or update, and you specified not to pass back the refreshed entity data when adding the operation to the sequence, then the result property will return null.
- If the operation was a remove, then the result property will always return null.
- If the operation resulted in an exception, then the result property will return an instance of Sys.Data.DataServiceError that contains information about the error that occurred.
For an explanation of the DataServiceError class see part 6 of this series. Knowing the above result criteria will make it so you can diagnose just about every possible scenario. Below is an example of a callback method that handles the completion of an ActionSequence’s execution:
function sequenceCompleted(results, hasError) { if (hasError) { for (var i = 0; i < results.length; i++) { var result = results[i]; if (Sys.Data.DataServiceError.isInstanceOfType(result.get_result())) { alert("The following " + result.get_operation() + " operation failed."); } } } else { alert("All operations succeeded."); } }
If no errors occurred, I alert the overall sequence success, otherwise I enumerate through every ActionResult instance to determine which operations erred, at which point I alert the error reason. You can see how you could do some pretty sophisticated exception handling if you wanted to. I’m not even checking for timeouts or examining the actual exception message, type, or stack trace. Needless to say, the ActionResult class, paired with the DataServiceError class, provides you with a nice amount of information.
Being able to use the ActionSequence class to create a unit of work is very beneficial. This coupled with the ability to make individual data modification requests, as we saw in the last article, makes for a very powerful and flexible environment for working with a data service using the ASP.NET AJAX libraries.
The ActionSequence class has one more method of interest: clearActions. If you for some reason need to flush your current ActionSequence, you can use the clearActions method like so:
sequence.clearActions();
You could imagine developing some pretty sweet AJAX applications that let the user create a series of data modifications, and then choose to undo them. I absolutely love the ADO.NET Data Services AJAX libraries. They provide you with some amazing power and functionality. Having an arsenal this strong in JavaScript makes it very compelling to move application logic to the client-side. Between WebDataContext, Sys.Data.DataService, and Sys.Data.ActionSequence, you can build some awesome data service client applications, regardless what the environment is. The only thing left to see is consuming a data service from within a Silverlight 2.0 application, which will be so beautiful I’ll most definitely shed a few tears.
In the last three articles we’ve seen how to perform data modification operations from a client’s perspective. What we haven’t seen yet are the service-side aspects that relate to data modification scenarios. In the next article, we will see just what has to be taken into consideration when creating a data service that needs to accept data modification requests.
0 Responses to “ADO.NET Data Services Part 9: Data Modification - AJAX (Action Sequences)”
Leave a Reply