Archives
-
Implementing InstanceDependencyProperty
If you were looking to the UCMA 3.0 documentation for information on how to use InstanceDependencyProperty, don't. Its incomplete and what is there is incorrect. In an effort to implement my custom activities in the same way as Microsoft, I wanted to ensure that property values didn't get destroyed when you moved on to the next activity in your workflow. This is vital if you plan on returning values from a Workflow Activity for another Activity to make use of. For example, you likely want the Recognition Result from the question you asked them so you can act on it. The InstanceDependencyProperty allows you to keep property changes you made within an Activity. Without it, the second the instance of that Activity is finished executing, the property changes are discarded. If you checked them from another activity, all you'd get are the default values. Not very useful if you ask me. So here is an stripped down example of an Activity that implements InstanceDependencyProperties properly: [csharp] using System.Workflow.ComponentModel; using System.Workflow.ComponentModel.Serialization; using Microsoft.Rtc.Workflow.Activities; namespace AudioRecordingActivity { public partial class Activity1 : Activity, IInstanceDependencyContainer { private static System.Collections.Generic.Dictionary _instanceDependencyProperties; public Activity1() { _instanceDependencyProperties = new System.Collections.Generic.Dictionary(); } public System.Collections.Generic.Dictionary InstanceDependencyProperties { get { return _instanceDependencyProperties; } } public readonly static InstanceDependencyProperty MyStringProperty = InstanceDependencyProperty.Register("MyString", typeof(string), typeof(Activity1), "Empty"); public string MyString { get { if (base.DesignMode) return InstanceDependencyHelper.GetValue(this, MyStringProperty).ToString(); else return InstanceDependencyHelper.GetValue(this, this.WorkflowInstanceId, this.MyStringProperty).ToString(); } set { if (base.DesignMode) InstanceDependencyHelper.SetValue(this, MyStringProperty, value); else return InstanceDependencyHelper.SetValue(this, this.WorkflowInstanceId, MyStringProperty, value); } } protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext) { MyString = "Test String"; return base.Execute(executionContext); } } } [/csharp] Here the key things to know:
- Your Activity needs to implement the IInstanceDependencyContainer interface
When you impement the interface, the private _instanceDependencyProperties dictionary must be static- Your properties must implement both prototypes of GetValue/SetValue. One is used for design-time, the other for run-time.
One other important note. In the documentation example they use a property with a TimeStamp type. You need to decorate this property with [TypeConverter(typeof(TimeSpanConverter))] or it will give you a very vague error at compile time. UPDATE: I had initially thought I had this fully working until I attempted to drop more than one of my custom activities on my workflow. I quickly found an issue with the static _instanceDependencyProperties however. Because it was static, changes to properties on Activity1 were reflected on Activity2. After some time with Reflector, looking at how Microsoft's activities were built, I noticed they used two different versions of InstanceDependencyHelper, one for design time and one for run time. Low and behold, if you implement both versions and remove the static statement you get the correct behavior. Its very unfortunate that the documentation doesn't explain this.
-
Building a New Record Audio Activity
One of the key activities missing from UCMA that we used in Speech Sever 2007 is the Record Audio Activity. This was one of my "go to" tools in the Speech Server arsenal, so I've been hurting without. So to eliminate my pain, and hopefully some other's, I've started building a replacement. While what I've built so far isn't nearly feature-complete, it will hopefully provide a suitable starting point for those looking to simply record some audio. Trust me, starting from scratch wasn't much fun. It may seem simple (and it really is) but it was extremely unclear when I started. A special thanks to Marshall Harrison who helped me put this together. What's Included
- InitialSilenceTimeout – How long should we record silence before we decide the calling isn't going to start talking
- EndingSilenceTimeout – How long should we record silence before we decided the caller has stopped talking
- FilePath – Full path where we should save the file (this is new, it wasn't definable in Speech Server)
- AudioEncoding – Format to encode the saved file as (Wma16Kbps,Wma48Kbps, Pcm16Khz or Pcm8Khz)
- RecordedLength - The length of the recorded made
What's Not Included
- LeadingSilenceTrimmedLength – We're just saving the raw audio, there is no post-processing of the file
- TrailingSilenceTrimmedLength – See above
- PlayBeep – This object simply records audio, it doesn't play any prompts or tones
- Prompts - See above
- TerminationDigits - The only method used for stopping recording at this point is silence from the caller
- CanBargeIn – Given the lack of prompts, there was nothing to "barge in" on
I've included the full project (and the compiled Debug and Release versions) here:Â AudioRecordingActivity1.2
UPDATE: I've fixed a few issues and updated the download to v1.1.Update: We're now at v1.2. This version solves some problems with Properties when multiple copies of the activity are in the same workflow -
UCMA 3 How-To: Inbound Call Throttling
Speech Server included a setting in the Administrator Console for inbound call throttling, simply set it and forget it. Here we'll discus how we can accomplish graceful inbound call throttling with UCMA 3.0. The basis for what we're discussing here is covered in the prior article "Decline A Call". We'll extend this code such that it checks for the number of calls currently connected to the application and gracefully declines calls with a busy signal we've reached capacity. One thing to understand about UCMA applications is that there are two objects in play here. The first is the Conversation object, the second is the Call object. Each Conversation instance can have one or more Calls associated with it. This is because (unlike Speech Server) UCMA supports conferencing. A conference is a single Conversation involving multiple Calls. For most IVR style applications however you'll have only a single Call per Conversation. For this reason (and because frankly its easier to understand) our sample here will be counting the number of "Conversations" rather than the number of "Calls". The ApplicationEndpoint class has a method called GetConverstations() which returns a generic collection of Conversation objects. When you create a new UCMA Workflow the default name for our ApplicationEnpoint is _endpoint. By executing _endpoint.GetConverstaions().Count we get an integer representing the number of Conversations active on this endpoint. Because we're making some assumptions with our application (a. there is a 1:1 ration or Conversations to Calls and b. we've got a single endpoint) we can test this value against our maximum threshold. For example, lets assume we have a maximum load of 50 calls:
if (_endpoint.GetConversations().Count > 50){
}
One note here, the call we are going decline here is included in GetConverstations().Count. We're not counting how many callers are within the Workflow, we counting the number of connections hitting the endpoint. This means there is a potential race condition here, if 50 people tried to call us at the exact same moment in time it would decline them all even though none of them had made it into the Workflow. Given how fast we reach this point it is unlikely, but it is something to keep in mind you are expecting that kind "instant load". In other words, American Idol needs a more nuanced method here. Now that we've got our count, we can simply Decline the call and return the status 486 (BUSY HERE) trigger a busy signal to the caller:
if (_endpoint.GetConversations().Count > 50) { call.Decline(new CallDeclineOptions(486)); return; }