Speech Server
Creating Customized Workflow Activities

Application authors can create voice response activities that are usable in more than one application and more than one workflow. Putting application functionality into separate components has the following advantages:

  • Components developed for one application can be used in another application.
  • Application development work can be allocated among team members more easily.
  • The graphic representation of workflows is reduced in size.
  • The size of workflows can be reduced and their load time can be decreased.
Three Customization Techniques

There are three techniques for workflow customization: two intended for general use and one intended for advanced tasks.

The first general technique is to add a Voice Response Sequence Activity to your application, and then drag and drop activities onto it. When the application is compiled, the activity creates an icon in the Toolbox, and this icon can be dragged onto any workflow in the application. This is the simplest customization technique and requires no further explanation in this context. The Voice Response Sequence Activity is derived from SpeechSequenceActivity.

The second general technique is to add a Voice Response Sequential Workflow to your application, and then drag and drop activities onto it. After the application is compiled, the activity can be called by other workflows in the application, using a SpeechServer.Dialog.InvokeWorkflowActivity activity. The Voice Response Sequential Workflow is derived from SpeechSequentialWorkflowActivity.

The advanced technique is to add a Voice Response Composite Activity to your application and customize it with code. When the application is compiled, the activity creates an icon in the Toolbox, and this icon can be dragged onto any workflow in the application. Custom activities created with this technique can access the application???s recognizer, synthesizer, and recorder objects directly and can invoke methods and handle events. Authors can use this technique to create their own custom versions of other workflow activities. The Voice Response Composite Activity is derived from SpeechCompositeActivity.

With all three customization techniques, the following guidelines apply:

  • Properties that are created on the custom activities should be InstanceDependencyProperty objects instead of ordinary properties.
  • The designer file of the application's main workflow contains a reference to the solution's prompts project. This reference should be copied and pasted into the InitializeComponent routine of the custom activity's designer file. For example:
    CSharp
    this.PromptDatabase = new System.Uri("Prompts\\MyApplication1Prompts.prompts", System.UriKind.Relative);

Customization Using a Voice Response Sequential Workflow

This section explains a simple custom workflow using the second customization technique. The custom workflow is created by adding a new item to the project, and then selecting Voice Response Sequential Workflow in the Add New Item dialog box. The custom workflow is called PlanetQuizzer.cs and has three properties: Question, Answer, and UserResponse. The calling workflow sets the Question and Answer properties. The custom workflow asks the Question to the user, saves the user's response in UserResponse, tells the user whether UserResponse matches Answer, and then returns UserResponse to the calling activity.

To support the run-time behavior of workflows, the properties of the custom workflow must be InstanceDependencyProperty objects rather than standard properties.

In the code of PlanetQuizzer.cs, the following code supports the three properties:

CSharp
        public static InstanceDependencyProperty QuestionProperty =
InstanceDependencyProperty.Register("Question", typeof(string), typeof(PlanetQuizzer));
        public static InstanceDependencyProperty AnswerProperty =
InstanceDependencyProperty.Register("Answer", typeof(string), typeof(PlanetQuizzer));
        public static InstanceDependencyProperty UserResponseProperty =
InstanceDependencyProperty.Register("UserResponse", typeof(string), typeof(PlanetQuizzer));

public PlanetQuizzer()
{
InitializeComponent();
}

        public string Question
        {
            get
            {
                return (string)GetValue(QuestionProperty);
            }
            set
            {
                SetValue(QuestionProperty, value);
            }
        }

        public string Answer
        {
            get
            {
                return (string)GetValue(AnswerProperty);
            }
            set
            {
                SetValue(AnswerProperty, value);
            }
        }

        public string UserResponse
        {
            get
            {
                return (string)GetValue(UserResponseProperty);
            }
            set
            {
                SetValue(UserResponseProperty, value);
            }
        }

The PlanetQuizzer workflow contains:

  • A QuestionAnswer activity that asks the Question to the user.
  • An IfElse activity that compares the user response with Answer.
  • Two Statement activities that save the user response in UserResponse and tell the user whether the response is correct.

This is the code for these activities.

CSharp
        private void askPlanet_TurnStarting(object sender, TurnStartingEventArgs e)
        {
            askPlanet.MainPrompt.SetText(Question);
        }

        private void sayAnswerIsCorrect_TurnStarting(object sender, TurnStartingEventArgs e)
        {
            sayAnswerIsCorrect.MainPrompt.SetText("That's right.");
            UserResponse = this.askPlanet.RecognitionResult.Semantics.Value.ToString();
        }

        private void sayAnswerIsIncorrect_TurnStarting(object sender, TurnStartingEventArgs e)
        {
            sayAnswerIsIncorrect.MainPrompt.SetText("I'm sorry.");
            UserResponse = this.askPlanet.RecognitionResult.Semantics.Value.ToString();
        }

The main, or invoking, workflow creates parameter bindings that enable PlanetQuizzer, the invoked workflow, to associate the passed parameters with its public properties. For each parameter to be passed, the code in the following example creates a WorkflowParameterBinding object, sets the ParameterName and Value properties on the WorkflowParameterBinding object, and then adds the WorkflowParameterBinding object to the ParameterBindings collection on the InvokeWorkflowActivity object.

CSharp
        public Workflow1()
        {
            InitializeComponent();
 
            // Set up to pass parameters to the invoked workflow
            WorkflowParameterBinding pb1 = new WorkflowParameterBinding();
            WorkflowParameterBinding pb2 = new WorkflowParameterBinding();

            // Set the name and value for the first parameter, and then add to the 
            // ParameterBindings property on the InvokeWorkflowActivity instance
            pb1.ParameterName = "Question";
            pb1.Value = "What planet is red?";
            invokeWorkflowActivity1.ParameterBindings.Add(pb1);

            // Set the name and value for the second parameter, and then add to the 
            // ParameterBindings property on the InvokeWorkflowActivity instance
            pb2.ParameterName = "Answer";
            pb2.Value = "Mars";
            invokeWorkflowActivity1.ParameterBindings.Add(pb2);
        }

After PlanetQuizzer, the invoked workflow, completes execution, an event handler in the invoking workflow (shown in the following example) takes the return value information packaged in Result, and reports its findings. The example code determines whether the Result dictionary contains keys named UserResponse and Answer, and if so, uses these keys to obtain the values associated with them.The code for these activities looks like this:

CSharp
        private void reportResults_TurnStarting(object sender, TurnStartingEventArgs e)
        {
            object responseValue = null;
            object answerValue = null;
            bool hasResponse = false;
            bool hasAnswer = false;
            IDictionary<string, object> response = invokeWorkflowActivity1.Result;
 
            if (response.ContainsKey("UserResponse") && response.ContainsKey("Answer") )
            {
                hasResponse = response.TryGetValue("UserResponse", out responseValue);
                hasAnswer = response.TryGetValue("Answer", out answerValue);
                if ((hasResponse == true) && (hasAnswer == true) && responseValue.Equals(answerValue))
                {
                    reportResults.MainPrompt.SetText("Congratulations " + responseValue.ToString() + " is correct");
                }
                else
                {
                    reportResults.MainPrompt.SetText(responseValue.ToString() + " is not correct");
                }
            }
            else 
            {
                reportResults.MainPrompt.SetText("Error - Result does not contain a UserResponse or Answer key");
            }
        }

        public static DependencyProperty invokeWorkflowActivity1_TargetWorkflow1Property = DependencyProperty.Register("invokeWorkflowActivity1_TargetWorkflow1", typeof(System.Type), typeof(InvokeWorkflow.Workflow1));

        [DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)]
        [BrowsableAttribute(true)]
        [CategoryAttribute("Speech")]
        public Type invokeWorkflowActivity1_TargetWorkflow1
        {
            get
            {
                return ((System.Type)(base.GetValue(InvokeWorkflow.Workflow1.invokeWorkflowActivity1_TargetWorkflow1Property)));
            }
            set
            {
                base.SetValue(InvokeWorkflow.Workflow1.invokeWorkflowActivity1_TargetWorkflow1Property, value);
            }
        }

        public static DependencyProperty invokeWorkflowActivity1_LogResults1Property = DependencyProperty.Register("invokeWorkflowActivity1_LogResults1", typeof(System.Boolean), typeof(InvokeWorkflow.Workflow1));

        [DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)]
        [BrowsableAttribute(true)]
        [CategoryAttribute("Speech")]
        public Boolean invokeWorkflowActivity1_LogResults1
        {
            get
            {
                return ((bool)(base.GetValue(InvokeWorkflow.Workflow1.invokeWorkflowActivity1_LogResults1Property)));
            }
            set
            {
                base.SetValue(InvokeWorkflow.Workflow1.invokeWorkflowActivity1_LogResults1Property, value);
            }
        }

Customization Using a Voice Response Composite Activity

This section discusses the advanced type of customized workflows. It is expected that authors will create this type of custom activities to perform asynchronous operations with the synthesizer, recognizer, and recorder objects. Generally, an asynchronous operation is performed in the ExecuteCore method of the activity, and information from the operation is returned to the activity by means of events. Authors must write a handler for each event that they want to handle, associate the handler with the appropriate event, and remove the reference to the event handler when it is no longer needed.

The code example for the ISynthesizer.SpeakAsync method is an example of this type of custom workflow activity. The asynchronous operation and its event-handling resemble the following code.

CSharp
        /// <summary>
        /// Executes the activity.
        /// </summary>
        /// <param name="executionContext">The execution context</param>
        protected override void ExecuteCore(ActivityExecutionContext executionContext)
        {
            // Prepare the PromptBuilder
            pb = new PromptBuilder();
            pb.AppendText(pt);

            // Specify the event-handler and play the prompt
            Workflow.Synthesizer.SpeakCompleted += Synthesizer_SpeakCompleted;
            Workflow.Synthesizer.SpeakAsync(pb);
        }
        /// <summary>
        /// Closes the activity.
        /// </summary>
        /// <param name="sender">The synthesizer</param>
        /// <param name="e">The result of SpeakAsync</param>
        private void Synthesizer_SpeakCompleted(object sender, SpeakCompletedEventArgs e)
        {
            // Remove this event-handler
            Workflow.Synthesizer.SpeakCompleted -= Synthesizer_SpeakCompleted;

            // End the custom activity
            Close(e.Error);
        }
Tags :


Page view tracker