13

I'm trying to create a dialog in which I define multiple waterfall steps. In the context of this dialog, I need sometimes to go back to the previous waterfall step according to the choice of the user. I found this method :

 await stepContext.ReplaceDialogAsync("Name of the dialog");

however, this method re-execute the whole dialog and this is not what I need.

In fact, the waterfall steps that I created are three :

  • ChoiceCallStepAsync: The first step will list the first 10 calls of the user with the options to show older steps
  • ShowCallStepAsync: The second step will show the call that the user choose or go back to the first step if the user clicked on "show older"
  • EndDialog: The third step will terminate the dialog

My code is :

public class ListAllCallsDialog : ComponentDialog
    {

        // Dialog IDs
        private const string ProfileDialog = "ListAllCallsDialog";



        /// <summary>
        /// Initializes a new instance of the <see cref="ListAllCallsDialog"/> class.
        /// </summary>
        /// <param name="loggerFactory">The <see cref="ILoggerFactory"/> that enables logging and tracing.</param>
        public ListAllCallsDialog(ILoggerFactory loggerFactory)
            : base(nameof(ListAllCallsDialog))
        {
            // Add control flow dialogs
            var waterfallSteps = new WaterfallStep[]
            {
                   ListAllCallsDialogSteps.ChoiceCallStepAsync,
                   ListAllCallsDialogSteps.ShowCallStepAsync,
                   ListAllCallsDialogSteps.EndDialog,
            };
            AddDialog(new WaterfallDialog(ProfileDialog, waterfallSteps));
            AddDialog(new ChoicePrompt("cardPrompt"));
        }

        /// <summary>
        /// Contains the waterfall dialog steps for the main dialog.
        /// </summary>
        private static class ListAllCallsDialogSteps
        {
            static int callListDepth = 0;
            static List<string> Calls;
            public static async Task<DialogTurnResult> ChoiceCallStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
            {
                await stepContext.Context.SendActivityAsync(
                   "Right now i'm in list all calls dialog",
                   cancellationToken: cancellationToken);
                GetAllCalls();
                return await stepContext.PromptAsync("cardPrompt", GenerateOptions(stepContext.Context.Activity, callListDepth), cancellationToken);
            }

            public static async Task<DialogTurnResult> ShowCallStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
            {
                // Get the text from the activity to use to show the correct card
                var text = stepContext.Context.Activity.Text.ToLowerInvariant();
                if(text == "Show older")
                    //Go back to the first step
                else if(text == "Show earlier")
                    //Go back to the first step
                else
                    await stepContext.Context.SendActivityAsync(
                   "The call you choose is : " + text.ToString(),
                   cancellationToken: cancellationToken);
                   return await stepContext.ContinueDialogAsync();

            }

            public static async Task<DialogTurnResult> EndDialog(WaterfallStepContext stepContext, CancellationToken cancellationToken)
            {
                await stepContext.Context.SendActivityAsync(
               "Getting back to the parent Dialog",
               cancellationToken: cancellationToken);
                return await stepContext.EndDialogAsync(null, cancellationToken);
            }

            /// <summary>
            /// Creates options for a <see cref="ChoicePrompt"/> so the user may select an option.
            /// </summary>
            /// <param name="activity">The message activity the bot received.</param>
            /// <returns>A <see cref="PromptOptions"/> to be used in a prompt.</returns>
            /// <remarks>Related type <see cref="Choice"/>.</remarks>
            private static PromptOptions GenerateOptions(Activity activity, int callListDepth)
            {
                // Create options for the prompt
                var options = new PromptOptions()
                {
                    Prompt = activity.CreateReply("Please choose a call from the list below"),
                    Choices = new List<Choice>(),
                };


                for(int i=10*callListDepth; i <= 10 * (callListDepth + 1); i++)
                {
                    if (Calls.ElementAtOrDefault(i) != null)
                      options.Choices.Add(new Choice() { Value = Calls[i] });

                }
                options.Choices.Add(new Choice() { Value = "Show older" });
                if(callListDepth!=0)
                    options.Choices.Add(new Choice() { Value = "Show earlier" });
                return options;
            }
            private static void GetAllCalls()
            {

                //List of all calls found
                for (int i = 0; i < 30; i++)
                  Calls.Add("Call" + i.ToString());
            }

        }

}

Can someone show me how to do this, please ?

Nicolas R
  • 13,812
  • 2
  • 28
  • 57
Soufien Hajji
  • 477
  • 1
  • 8
  • 24

3 Answers3

19

I'm not sure, if it's the right and efficient way to do it, but you can experiment with the State property of the context.ActiveDialog within your Task<DialogTurnResult> function.

context.ActiveDialog.State["stepIndex"] = (int)context.ActiveDialog.State["stepIndex"] -2;
Liam Kernighan
  • 2,335
  • 1
  • 21
  • 24
  • 2
    This works. If ( need to go back) then stepContext.ActiveDialog.State["stepIndex"] = (int)stepContext.ActiveDialog.State["stepIndex"] - 2; return await AskStep1Async(stepContext, cancellationToken); – Oyen Oct 15 '18 at 22:22
  • 2
    @Oyen I'm just calling NextAsync or ContiniueDialogAsync as usual without any problems. -1 if I want to return to the same dialog that I'm currently in, and built in validation doesn't fit for some reason, and -2 if want to go back to the previous one. – Liam Kernighan Oct 16 '18 at 00:41
  • Yes, I was showing support to your answr because they still did not mark it as Answer. – Oyen Oct 16 '18 at 04:34
5

Waterfall dialogs weren't designed with the idea of 'going backwards' to traverse them, though I can see the possible need to. The only solution I've found is to break your waterfall into smaller "mini" waterfalls, and nest them into one larger waterfall.

        // define and add waterfall dialogs (main)
        WaterfallStep[] welcomeDialogSteps = new WaterfallStep[]
        {
            MainDialogSteps.PresentMenuAsync,
            MainDialogSteps.ProcessInputAsync,
            MainDialogSteps.RepeatMenuAsync,
        };

Then in MainDialogSteps.ProcessInputAsync:

        public static async Task<DialogTurnResult> ProcessInputAsync(
            WaterfallStepContext stepContext,
            CancellationToken cancellationToken)
        {
            var choice = (FoundChoice)stepContext.Result;
            var dialogId = Lists.WelcomeOptions[choice.Index].DialogName;

            return await stepContext.BeginDialogAsync(dialogId, null, cancellationToken);
        }

This allows the users to start new dialogs still within the main dialog stack. One of my options I offered was a prompt of a list of phone numbers:

        WaterfallStep[] phoneChoiceDialogSteps = new WaterfallStep[]
        {
            PhoneChoicePromptSteps.PromptForPhoneAsync,
            PhoneChoicePromptSteps.ConfirmPhoneAsync,
            PhoneChoicePromptSteps.ProcessInputAsync,
        };

        Add(new WaterfallDialog(Dialogs.PhonePrompt, phoneChoiceDialogSteps));

And finally, in the PhoneChoicePromptSteps.ProcessInputAsync, I allowed for the selection of 'no' from the confirm to ReplaceDialogAsync and effectivly reset this smaller waterfall, without effecting the rest of the overall waterfall:

 public static async Task<DialogTurnResult> ProcessInputAsync(
            WaterfallStepContext stepContext,
            CancellationToken cancellationToken)
        {
            if ((bool)stepContext.Result)
            {
                await stepContext.Context.SendActivityAsync(
                    $"Calling {stepContext.Values[Outputs.PhoneNumber]}",
                    cancellationToken: cancellationToken);
                return await stepContext.EndDialogAsync(null, cancellationToken);
            }
            else
            {
                return await stepContext.ReplaceDialogAsync(Dialogs.PhonePrompt, null, cancellationToken);
            } 
        }

enter image description here

JJ_Wailes
  • 2,207
  • 1
  • 9
  • 17
  • I understand. In a nutshell putting the waterfall step that is ought to be re-executed in the head of a mini smaller waterfall steps should do the trick. This way re-executing the this waterfall step is done by replacing the dialog that contains the mini waterfall steps by itself in the final waterfall step. – Soufien Hajji Oct 18 '18 at 13:12
  • 3
    Exactly. That way, your user can then proceed along the dialog with the bot, and if further "You chose XYZ, confirm?" steps are needed, you can loop around that as well. – JJ_Wailes Oct 18 '18 at 14:59
1

You can use the option parameter in the method "ReplaceDialogAsync" and skip steps with the method "NextAsync".

For example in my waterfall steps (defined in the constructor):

        AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[]
        {
            IntroStepAsync,
            ActStepAsync,
            FinalStepAsync
        }));

        // The initial child Dialog to run.
        InitialDialogId = nameof(WaterfallDialog);

If you want pass to second step (in my case ActStepAsync) from the final step (FinalStepAsync) , when I going to replace the dialog I use the created a label in the Dialog:

private const string FLAG = "MY_FLAG";

When I invoke the method from final step I do this :

return await stepContext.ReplaceDialogAsync(InitialDialogId, FLAG, cancellationToken);

So I only need check the option in the first step if the context has the flag :

    // Use the text provided in FinalStepAsync or the default if it is the first time.
        var messageText = stepContext.Options?.ToString() ?? "welcome-message";
        if (messageText.Equals(FLAG_REPROMPT))
        {
            return await stepContext.NextAsync(null,cancellationToken);
        }

Later this you are in the second step