Handling event arguments in MSS managed API

POSTED BY: GUNNAR KUDRJAVETS, MSS Software Design Engineer
ALAN TURNQUIST, MSS Software Design Engineer

As most of the code I’ve written during last 1.5 years has been using our managed API, I’ve been starting to notice some general patterns which introduce defects. One of the common coding mistakes I’ve personally made multiple times while using our managed API was handling arguments from the various delegates incorrectly. The following applies to different types of arguments which inherit from AsyncCompletedEventArgs: RecognizeCompletedEventArgs, RecordCompletedEventArgs, SpeakCompletedEventArgs, etc. Let’s look at the typical example. Somewhere in your code you’ll subscribe to the event handler for RecognizeCompleted:


someObject.TelephonySession.SpeechRecognizer.RecognizeCompleted += OnRecognizeCompleted;

The usual event handler will look something like this:

private void OnRecognizeCompleted(object sender, RecognizeCompletedEventArgs args)
{
Debug.Assert(null != sender, "null != sender");
Debug.Assert(null != args, "null != args");
...
// Your code.
}

The important thing to take into account is the fact that if the asynchronous operation was cancelled, i.e., args.Canceled is true, then accessing any other property except args.Error will throw InvalidOperationException. Asynchronous operation could have been cancelled because you explicitly requested it, e.g. by calling RecognizeAsyncStop(), or because some error occurred while performing the specific operation. Please note that this doesn’t mean that args.Error will always be non-null. It may or it may not.

After checking args.Canceled you’ll also need to check for args.Error. In case args.Error != null then accessing any properties except Canceled will throw Exception object describing the error that happened previously.

I typically made mistakes by trying to log (for debugging purposes) the various properties of RecognizeCompletedEventArgs object without checking the state of the object first, or by trying to access properties like Result without checking the state of the args. You will also need to be careful when trying to access properties like RecordingSize which will throw if audio wasn’t retained during speech recognition.

The obvious question to ask is: why does the API behave this way? Why can’t these properties just return null or 0 in case there’s no value to return? Our decision to implement these properties this way (throw in case of unexpected access) was based on the consistency with other .NET classes. Here’s the official documentation for RaiseExceptionIfNecessary from MSDN:

Notes to Inheritors
If you have derived your own class from the AsyncCompletedEventArgs class, your read-only properties should call the RaiseExceptionIfNecessary method before returning the property value. If the component's asynchronous worker code assigns an exception to the Error property or sets the Cancelled property to true, the property will raise an exception if a client tries to read its value. This prevents clients from accessing properties that are potentially not valid due to a failure in the asynchronous operation.

As a summary:

1) When handling different properties of the event arguments classes, always handle the cases with args.Canceled == true and args.Error != null first and then proceed to the normal code path. It might even be a good preventive measure to assert those invariants before your code handles the successful case.

2) Look carefully at all the properties for different event arguments classes to understand under what conditions they are accessible. Believe me, those extra minutes are worth your time and will prevent you from introducing bugs which will later take hours to fix ;-)

Here’s the code pattern you may wish to use:

private void OnRecognizeCompleted(object sender, RecognizeCompletedEventArgs args)
{
Debug.Assert(null != sender, "null != sender");
Debug.Assert(null != args, "null != args");

if (args.Canceled)
{
...
// Operation was cancelled.
}
else if (null != args.Error)
{
...
// Something went wrong during the operation.
}
else
{
Debug.Assert(!args.Canceled, "!args.Canceled");
Debug.Assert(null == args.Error, "null == args.Error");

...
// Speech recognizer returned some kind of result.
}

...
}