Search code examples
c#botframework

Botframework resume dialog not called


I have a problem with my component dialogs in botframework V4.

I'm using a root dialog which contains a Waterfalldialog and an Setupdialog. The initial dialog is the Setupdialog. Like so:

public RootDialog(SetupDialog setupDialog)
            : base(nameof(RootDialog))
        {
            AddDialog(setupDialog);
            AddDialog(new TextPrompt(nameof(TextPrompt)));
            AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[]
            {
                ProcessStep
            }));
            InitialDialogId = nameof(SetupDialog);
        }

In the setup dialog I'm asking for some value. If the setup dialogs continues I check the value and if its like I wanted, I end the dialog.

public override Task<DialogTurnResult> ContinueDialogAsync(DialogContext outerDc, CancellationToken cancellationToken = default)
        {
            if (!int.TryParse(outerDc.Context.Activity.Text, out var recursions))
            {
                return outerDc.PromptAsync(nameof(TextPrompt), new PromptOptions()
                {
                    Prompt = MessageFactory.Text($"{outerDc.Context.Activity.Text} konnte nicht in eine Zahl konvertiert werden.")
                });
            }

            return outerDc.EndDialogAsync(recursions);
        }

If I end the dialog like this, shouldn't be ResumeDialog be called in the RootDialog?

Here the whole Dialogs:

public class RootDialog : ComponentDialog
    {
        private int recursions;

        public RootDialog(SetupDialog setupDialog)
            : base(nameof(RootDialog))
        {
            AddDialog(setupDialog);
            AddDialog(new TextPrompt(nameof(TextPrompt)));
            AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[]
            {
                ProcessStep
            }));
            InitialDialogId = nameof(SetupDialog);
        }

        public async Task<DialogTurnResult> ProcessStep(WaterfallStepContext stepContext, CancellationToken cancellationToken)
        {

            await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions
            {
                Prompt = MessageFactory.Text("Process step in root dialog")
            });

            if(Dialogs.Find(nameof(RecursiveDialog)) == null)
            {
                AddDialog(new RecursiveDialog(new DialogSet(), recursions));
            }

            if (recursions > 0)
            {
                return await stepContext.BeginDialogAsync(nameof(RecursiveDialog));
            }

            return await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions
            {
                Prompt = MessageFactory.Text("Recursion lower or eqaul 0")
            });
        }

        public override async Task<DialogTurnResult> BeginDialogAsync(DialogContext outerDc, object options = null, CancellationToken cancellationToken = default)
        {
            var dialogContext = CreateChildContext(outerDc);
            await dialogContext.PromptAsync(nameof(TextPrompt), new PromptOptions
            {
                Prompt = MessageFactory.Text("Begin root dialog")
            });

            return await base.BeginDialogAsync(outerDc, options, cancellationToken);
        }

        public override async Task<DialogTurnResult> OnContinueDialogAsync(DialogContext innerDc, CancellationToken cancellationToken = default)
        {
            if(innerDc.ActiveDialog != null)
            {
                return await innerDc.ContinueDialogAsync();
            }

            return await base.OnContinueDialogAsync(innerDc, cancellationToken);
        }

        public override async Task<DialogTurnResult> ResumeDialogAsync(DialogContext outerDc, DialogReason reason, object result = null, CancellationToken cancellationToken = default)
        {
            recursions = Convert.ToInt32(result);

            await outerDc.PromptAsync(nameof(TextPrompt), new PromptOptions
            {
                Prompt = MessageFactory.Text($"Resume root dialog with recursion value {recursions}")
            });

            return await outerDc.BeginDialogAsync(nameof(WaterfallDialog));
        }
    }

SetupDialog:

 public class SetupDialog : ComponentDialog
    {
        public SetupDialog()
            : base(nameof(SetupDialog))
        {
            AddDialog(new TextPrompt(nameof(TextPrompt)));
        }

        public override Task<DialogTurnResult> ContinueDialogAsync(DialogContext outerDc, CancellationToken cancellationToken = default)
        {
            if (!int.TryParse(outerDc.Context.Activity.Text, out var recursions))
            {
                return outerDc.PromptAsync(nameof(TextPrompt), new PromptOptions()
                {
                    Prompt = MessageFactory.Text($"{outerDc.Context.Activity.Text} konnte nicht in eine Zahl konvertiert werden.")
                });
            }

            return outerDc.EndDialogAsync(recursions);
        }

        public override Task<DialogTurnResult> OnBeginDialogAsync(DialogContext innerDc, object options, CancellationToken cancellationToken = default)
        {
            return innerDc.PromptAsync(nameof(TextPrompt), new PromptOptions()
            {
                Prompt = MessageFactory.Text("Wie viel rekursionen sollen erstellt werden?")
            });
        }
    }

Solution

  • So, I got the ResumeDialog method to be triggerd.

    To trigger the ResumeDialog method, the dialog that ended and the dialog you want to resume to has to be on the same dialog stack!

    My scenario were BotDialogContext[RootDialog] -> RootDialogContext[SetupDialog], but I need a context like this BotDialogContext[RootDialog, SetupDialog].

    One of the problems is, that every ComponentDialog you start creates its own DialogContext. So if you begin a dialog within a dialog its pushed on the stack of the inner DialogContext and so on. But the description of the ResumeDialog method is

    Called when a child dialog on the parent's dialog stack completed this turn, returning control to this dialog component.

    To put an child dialog on the parents dialog stack you had to call the BeginDialog method on the outer dialog context. This context also needs to have the "child dialog" in its dialogset.

    Here my example:

    RootDialog.cs:

    public class RootDialog : ComponentDialog
    {
        private int recursions;
    
        public RootDialog(SetupDialog setupDialog)
            : base(nameof(RootDialog))
        {
            AddDialog(setupDialog);
            AddDialog(new TextPrompt(nameof(TextPrompt)));
            AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[]
            {
                ProcessStep
            }));
            InitialDialogId = nameof(SetupDialog);
        }
    
        public async Task<DialogTurnResult> ProcessStep(WaterfallStepContext stepContext, CancellationToken cancellationToken)
        {
    
            await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions
            {
                Prompt = MessageFactory.Text("Process step in root dialog")
            });
    
            if(Dialogs.Find(nameof(RecursiveDialog)) == null)
            {
                AddDialog(new RecursiveDialog(new DialogSet(), recursions));
            }
    
            if (recursions > 0)
            {
                return await stepContext.BeginDialogAsync(nameof(RecursiveDialog));
            }
    
            return await stepContext.PromptAsync(nameof(TextPrompt), new PromptOptions
            {
                Prompt = MessageFactory.Text("Recursion lower or eqaul 0")
            });
        }
    
        public override async Task<DialogTurnResult> BeginDialogAsync(DialogContext outerDc, object options = null, CancellationToken cancellationToken = default)
        {
            if (true)
            {
                return await outerDc.BeginDialogAsync(nameof(SetupDialog));
            }
    
            var dialogContext = CreateChildContext(outerDc);
            await dialogContext.PromptAsync(nameof(TextPrompt), new PromptOptions
            {
                Prompt = MessageFactory.Text("Begin root dialog")
            });
    
            return await base.BeginDialogAsync(outerDc, options, cancellationToken);
        }
    
        protected override async Task<DialogTurnResult> OnContinueDialogAsync(DialogContext innerDc, CancellationToken cancellationToken = default)
        {
            if(innerDc.ActiveDialog != null)
            {
                return await innerDc.ContinueDialogAsync();
            }
            return await base.OnContinueDialogAsync(innerDc, cancellationToken);
        }
    
        public override async Task<DialogTurnResult> ResumeDialogAsync(DialogContext outerDc, DialogReason reason, object result = null, CancellationToken cancellationToken = default)
        {
            recursions = Convert.ToInt32(result);
    
            await outerDc.PromptAsync(nameof(TextPrompt), new PromptOptions
            {
                Prompt = MessageFactory.Text($"Resume root dialog with recursion value {recursions}")
            });
    
            return await outerDc.BeginDialogAsync(nameof(WaterfallDialog));
        }
    }
    

    SetupDialog.cs:

    public class SetupDialog : ComponentDialog
    {
        public SetupDialog()
            : base(nameof(SetupDialog))
        {
            AddDialog(new TextPrompt(nameof(TextPrompt)));
        }
    
        public override Task<DialogTurnResult> ContinueDialogAsync(DialogContext outerDc, CancellationToken cancellationToken = default)
        {
            if (!int.TryParse(outerDc.Context.Activity.Text, out var recursions))
            {
                return outerDc.PromptAsync(nameof(TextPrompt), new PromptOptions()
                {
                    Prompt = MessageFactory.Text($"{outerDc.Context.Activity.Text} konnte nicht in eine Zahl konvertiert werden.")
                });
            }
    
            return outerDc.EndDialogAsync(recursions);
        }
    
        public override Task<DialogTurnResult> BeginDialogAsync(DialogContext outerDc, object options = null, CancellationToken cancellationToken = default)
        {
            return base.BeginDialogAsync(outerDc, options, cancellationToken);
        }
    
        protected override Task<DialogTurnResult> OnBeginDialogAsync(DialogContext innerDc, object options, CancellationToken cancellationToken = default)
        {
            return innerDc.PromptAsync(nameof(TextPrompt), new PromptOptions()
            {
                Prompt = MessageFactory.Text("Wie viel rekursionen sollen erstellt werden?")
            });
        }
    }
    

    DialogBot.cs:

    public class DialogBot<T> : ActivityHandler
        where T : Dialog
    {
        protected readonly DialogSet Dialogs;
        protected readonly BotState ConversationState;
        protected readonly BotState UserState;
        protected readonly ILogger Logger;
    
        public DialogBot(ConversationState conversationState, UserState userState, IEnumerable<Dialog> dialogs, ILogger<DialogBot<T>> logger)
        {
            ConversationState = conversationState;
            UserState = userState;
            Logger = logger;
    
            Dialogs = new DialogSet(conversationState.CreateProperty<DialogState>(nameof(DialogState)));
            foreach(var dialog in dialogs)
            {
                Dialogs.Add(dialog);
            }
        }
    
        public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
        {
            await base.OnTurnAsync(turnContext, cancellationToken);
    
            // Save any state changes that might have occured during the turn.
            await ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
            await UserState.SaveChangesAsync(turnContext, false, cancellationToken);
        }
    
        protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
        {
            Logger.LogInformation("Running dialog with Message Activity.");
    
            var dc = await Dialogs.CreateContextAsync(turnContext, cancellationToken).ConfigureAwait(false);
    
            if (dc.ActiveDialog != null)
            {
                await dc.ContinueDialogAsync();
            }
            else
            {
                // Run the Dialog with the new message Activity.
                await dc.BeginDialogAsync(typeof(T).Name, ConversationState.CreateProperty<DialogState>("DialogState"), cancellationToken);
            }
        }
    }
    

    Inside the IEnumerable are both (RootDialog & SetupDialog) to get both dialogs into to the BotDialogContext and DialogSet