Reusable Dialog Components Revisited

A while back we added support in VoiceModel for Reusable Dialog Components (RDC), which I discussed in a previous blog post.  It was an effective implementation but I kept thinking there must be an easier way to create and use RDC's.  After a looking more closely at SCXML and state machines the answer came to me; composite state machines.  Harel State Machines allow for superstates which are composed of nested state machines.  When a superstate becomes active it first runs the nested state machine that is composed of.  This seemed like a natural fit for RDC's; just make them a nested state machine of the main state machine (call flow).  VoiceModel has been updated to provide support for composite state machines. A lot of other changes have been made to VoiceModel's state machine to make it easier to persist to something like SCXML, actions are now implemented with delegates or lambda expression, and conditions on transitions now support expressions written in Javascript by using IronJS.  I will discuss more on these other features in future posts but this discussion will be on how these changes affect creating and using RDC's.

In the previous implementation RDC's were implemented as separate MVC controllers. This caused a lot of issues with moving back and forth between controllers, passing data between controller, and using portable areas to dynamically include controllers from another project.  Now that they are implemented as composite state machines it is much simpler.  To create an RDC you just create a class library project as before but instead of having your main class inherit from VoiceController you inherit from CallFlow. The CallFlow class represents a state machine that controls the dynamics of your voice application.  Lets look at the code for creating an RDC that retrieves a date from the user using DTMF.  This in the examples you will find in the source code for the VoiceModel Project.

   public class GetDateDtmfRDC : CallFlow
    {
        public GetDateDtmfRDC(Prompt AskDatePrompt)
        {
            BuildCallFlow(AskDatePrompt);

        }

        private void BuildCallFlow(Prompt AskDatePrompt)
        {
            AddState(ViewStateBuilder.Build("getDate", "validateDate", 
                new Ask("getDate", AskDatePrompt, new Grammar("digits?minlength=6"))), true);
            AddState(new State("validateDate", "confirmDate")
                .AddTransition("error","invalidDate",null)
                .AddOnEntryAction(delegate(CallFlow cf, State state, Event e)
                {
                    ValidateDate validator = new ValidateDate(cf, state);
                    validator.Validate();
                }));
            Prompt confirmPrompt = new Prompt();
            confirmPrompt.audios.Add(new TtsMessage("You Entered"));
            confirmPrompt.audios.Add(new TtsVariable("d.Month"));
            confirmPrompt.audios.Add(new TtsVariable("d.Day"));
            confirmPrompt.audios.Add(new TtsVariable("d.Year"));

            AddState(ViewStateBuilder.Build("confirmDate", 
              new Say("confirmDate", confirmPrompt)));
            AddState(ViewStateBuilder.Build("invalidDate", 
              new Say("invalidDate", "You entered and invalid date.")));
        }

        public GetDateDtmfOutput GetResults()
        {
            return ctx.GetGlobalAs<GetDateDtmfOutput>("GetDateDtmfOutput");
        }

    }


I created a constructor for this class that accepts any configuration items that are needed to build your state machine and views and then create them upon object construction. I also created a method called GetDateDtmfOutput to encapsulate retrieval of information that needs to be sent back to the superstate that contains this nested state or RDC.  The state machine in VoiceModel has added the concept of a context to maintain the state of data that is manipulated by the state machine.  This is represented by the ctx object which is the same type of context object used by IronJS. This make it easy to use Javascript to manipulate and evaluate any data in the context and will make it that much easier to implement SCXML support for evaluating conditions and script tags.

One of the many changes you will notice is that the view models that represent the VoiceXML are added to the appropriate states as a data model for that state instead of a separate collection that the state machine retrieved the information from. This makes the system much more efficient and allows for reuse of view-models across states.

You can also see how actions have changed for entry into the state and exiting the state.  You can add multiple actions for entering and exiting states and they are added as delegates or lambda expressions.  In this example the validateDate state uses an anonymous delegate to validate and format the date just entered by the user.

That is all there is to creating an RDC. So how do we use an RDC?  That is also simple.  Here is some code from the VoiceModel examples that demonstrates this.

flow.AddState(new State("getStartDate", "getFinishDate")
  .AddNestedCallFlow(new GetDateDtmfRDC(
      new Prompt("Enter the start date as a six digit number.")))
  .AddOnExitAction(delegate(CallFlow cf, State state, Event e)
  {cf.Ctx.SetGlobal<GetDateDtmfOutput>("StartDate",
      ((GetDateDtmfRDC)state.NestedCF).GetResults());}));


In this example the RDC is added as nested call flow by using the AddNestedCallFlow method of a State.  You just create a new instance of GetDateDtmfRDC and pass in the custom prompt used to ask the caller to enter the date. You will notice that I use an on-exit action to move the results of this nested state machine to the context of the parent state machine for later processing.

If you tried the old method of creating and using RDC's I think you will find this new method much easier and more robust.  I am interested in any feedback on this method and encourage you to try creating your own RDC using the VoiceModel Project.  It is a long term goal of the VoiceModel project to create a library of RDC's and I believe these changes move this goal forward.

Comments

Popular posts from this blog

Using Claims in ASP.NET Identity

Seeding & Customizing ASP.NET MVC SimpleMembership

Customizing Claims for Authorization in ASP.NET Core 2.0