简体   繁体   中英

Stackoverflow Exception in a simple dialog

Hello i am getting an Stackoverflow Exception in this two dialog. Dialog A is being called from a main dialog class. Dialog A have a choice to go to Dialog A child and Dialog A child has a choice to go back to Dialog A . But it is getting a Stackoverflow Exception. When i remove one from the other: Example removing Dialog A child from Dialog A or removing Dialog A from Dialog A child , the exception error disappears. In short it throw a Stackoverflow Exception when both dialog can call each other.

I know i can just EndDialogAsync in this specific scenario to go back to Dialog A but my real dialog flow is not together like this . How to fix this?

Dialog A code:

 public class DialogA : ComponentDialog
{
    private const string InitialId = "dialogA";
    private const string ChoicePrompt = "choicePrompt";
    private const string DialogAchildId = "dialogA_childId";

    public DialogA(string dialogId)
        : base(dialogId)
    {
        InitialDialogId = InitialId;

        WaterfallStep[] waterfallSteps = new WaterfallStep[]
         {
            FirstStepAsync,
            SecondStepAsync,
            ThirdStepAsync,
         };
        AddDialog(new WaterfallDialog(InitialId, waterfallSteps));
        AddDialog(new ChoicePrompt(ChoicePrompt));
        AddDialog(new DialogA_child(DialogAchildId));

    }

    private static async Task<DialogTurnResult> FirstStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken = default(CancellationToken))
    {
        return await stepContext.PromptAsync(
            ChoicePrompt,
            new PromptOptions
            {
                Prompt = MessageFactory.Text($"Here are your choices:"),
                Choices = new List<Choice>{new Choice { Value = "Open Dialog A_Child", }, new Choice { Value = "Open Dialog B_Child" }, },
                RetryPrompt = MessageFactory.Text($"Please choose one of the options."),
            });
    }

    private static async Task<DialogTurnResult> SecondStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken = default(CancellationToken))
    {
        var response = (stepContext.Result as FoundChoice)?.Value.ToLower();

        if (response == "open dialog a_child")
        {
            return await stepContext.BeginDialogAsync(DialogAchildId, cancellationToken: cancellationToken);
        }

        return await stepContext.NextAsync();
    }

    private static async Task<DialogTurnResult> ThirdStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken = default(CancellationToken))
    {
        return await stepContext.EndDialogAsync();
    }

Dialog A child code:

 public class DialogA_child : ComponentDialog
{
  private const string InitialId = "dialogAchild";
    private const string ChoicePrompt = "choicePrompt";
    private const string DialogAId = "dialogAId";

    public DialogA_child(string dialogId)
        : base(dialogId)
    {
        InitialDialogId = InitialId;

        WaterfallStep[] waterfallSteps = new WaterfallStep[]
         {
            FirstStepAsync,
            SecondStepAsync,
         };
        AddDialog(new WaterfallDialog(InitialId, waterfallSteps));
        AddDialog(new DialogA(DialogAId));

    }

    private static async Task<DialogTurnResult> FirstStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken = default(CancellationToken))
    {
        return await stepContext.PromptAsync(
       ChoicePrompt,
       new PromptOptions
       {
           Prompt = MessageFactory.Text($"Here are your choices:"),
           Choices = new List<Choice> {new Choice { Value = "Open Dialog A" }, new Choice { Value = "Open Dialog B" }, },
           RetryPrompt = MessageFactory.Text($"Please choose one of the options."),
       });
    }

    private static async Task<DialogTurnResult> SecondStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken = default(CancellationToken))
    {
        var response = (stepContext.Result as FoundChoice)?.Value.ToLower();

        if (response == "open dialog a")
        {
            return await stepContext.BeginDialogAsync(DialogAId, cancellationToken: cancellationToken);
        }

        return await stepContext.NextAsync();
    }

Main code that called dialog A:

   public class MainDialog : ComponentDialog
{
    private const string InitialId = "mainDialog";
    private const string ChoicePrompt = "choicePrompt";
    private const string DialogAId = "dialogAId";

    public MainDialog(string dialogId)
        : base(dialogId)
    {
        InitialDialogId = InitialId;

        WaterfallStep[] waterfallSteps = new WaterfallStep[]
         {
            FirstStepAsync,
            SecondStepAsync,
            ThirdStepAsync,
         };
        AddDialog(new WaterfallDialog(InitialId, waterfallSteps));
        AddDialog(new ChoicePrompt(ChoicePrompt));
        AddDialog(new DialogA(DialogAId));
    }

    private static async Task<DialogTurnResult> FirstStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken = default(CancellationToken))
    {
        return await stepContext.PromptAsync(
            ChoicePrompt,
            new PromptOptions
            {
                Prompt = MessageFactory.Text($"Here are your choices:"),
                Choices = new List<Choice>{ new Choice { Value = "Open Dialog A" }, new Choice { Value = "Open Dialog B" }, },
                RetryPrompt = MessageFactory.Text($"Please choose one of the options."),
            });
    }

    private static async Task<DialogTurnResult> SecondStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken = default(CancellationToken))
    {
        var response = (stepContext.Result as FoundChoice)?.Value.ToLower();

        if (response == "open dialog a")
        {
            return await stepContext.BeginDialogAsync(DialogAId, cancellationToken: cancellationToken);
        }

        if (response == "open dialog b")
        {
        }

        return await stepContext.NextAsync();
    }

    private static async Task<DialogTurnResult> ThirdStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken = default(CancellationToken))
    {

        return await stepContext.EndDialogAsync();
    }

In Visual Studio you can check your call stack and you will know where is the exact problem in your StackOverflowException .

If DialogA is the base class of DialogA_child then in your DialogA_child 's constructor and your base class constructor will be calling themselfs recursively.

So your call stack should look like this:

  1. DialogA constructor add new DialogA_child
  2. DialogA_child calls base(so DialogA constructor )
  3. DialogA constructor add new DialogA_child
  4. DialogA_child calls base(so DialogA constructor )
  5. ...

@koviroli's answer is 100% correct, so please accept his as the answer once you feel like you understand it. I'm adding this as an additional answer because it seems like you're struggling to understand things a little and comments limit me from providing good explanation.

Quick Explanation of Constructors

Since you're new to C#, I'll provide a quick explanation of constructors. DialogA_child 's constructor is this part:

public DialogA_child(string dialogId)
    : base(dialogId)
{
    InitialDialogId = InitialId;

    WaterfallStep[] waterfallSteps = new WaterfallStep[]
     {
        FirstStepAsync,
        SecondStepAsync,
     };
    AddDialog(new WaterfallDialog(InitialId, waterfallSteps));
    AddDialog(new DialogA(DialogAId));

}

Any time that you use new DialogA_child("xyz") , the constructor is called to "construct" DialogA_child . The :base(dialogId) makes it so that "xyz" gets sent to the constructor of DialogA_child 's base class, which is ComponentDialog . In ComponentDialog 's constructor, it sets the argument that gets passed in ("xyz", in this case) to the dialogId.

If you click on ComponentDialog in your code and press F12, it will take you to the definition of ComponentDialog and you can see this:

public ComponentDialog(string dialogId); .

Here's What's Wrong

In DialogA_child 's constructor, you have: AddDialog(new DialogA(DialogAId)); , which creates a new instance of DialogA . Then, in DialogA 's constructor, you have AddDialog(new DialogA_child(DialogAchildId)); , which create another DialogA_child , and so on and so on.

Basically, DialogA and DialogA_child keep creating new instances of each other, causing the StackOverflow.

The easiest fix is to just remove AddDialog(new DialogA(DialogAId)); .

Additional Notes

Again, I know you're new to C#, so I'll help you out with a couple of other things.

private const string ChoicePrompt = "choicePrompt";

should probably be

private const string ChoicePromptId = "choicePrompt";

since ChoicePrompt is already defined as a type of prompt.

When defining your dialog constructors, it's easiest to use something like:

public DialogA() : base(nameof(DialogA))

This will automatically set the id of DialogA to "DialogA". It will help with two things:

  1. Since dialogs have to use unique IDs, this will prevent you from accidentally calling the same dialog twice.

  2. It's easier to keep track of and you don't have to pass in a name for it. For example, to call the dialog, you could now use AddDialog(new DialogA()); instead of AddDialog(new DialogA(DialogAId)); .

Forcing a Dialog Loop

Currently, you cannot loop dialogs the way that you want to (See update below). You cannot:

  1. Have DialogA call DialogA_child
  2. Then have DialogA_child again call DialogA .

As you have seen, this creates a stack overflow.

Instead, you can call it indirectly.

Instead of having DialogA_child call DialogA , do something like:

  1. Have DialogA_child 's choice prompt with an option of "Restart Dialog A" (or something unique).
  2. In OnTurnAsync (in your bot's main Class file), check to see if the user responded with "Restart Dialog A". If so, clear all dialogs (or just replace) and then begin DialogA again.

Code:

DialogA_child :

private static async Task<DialogTurnResult> FirstStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken = default(CancellationToken))
{
    return await stepContext.PromptAsync(
    choicePrompt,
    new PromptOptions
    {
        Prompt = MessageFactory.Text($"Here are your choices:"),
        Choices = new List<Choice> { new Choice { Value = "Restart Dialog A" }, new Choice { Value = "Open Dialog B" }, },
        RetryPrompt = MessageFactory.Text($"Please choose one of the options."),
    });
}

<myBot>.cs :

public async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
    var activity = turnContext.Activity;

    var dc = await Dialogs.CreateContextAsync(turnContext);

    if (activity.Text == "Restart Dialog A")
    {
        await dc.CancelAllDialogsAsync();
        await dc.BeginDialogAsync(nameof(DialogA));
    }

Update

BotBuilder SDK V4.3 will release soon that allows any child or sibling dialog to call any dialog defined by the parent. See this pull request . I believe you can have a child dialog call a parent, as OP requested, but it's still new and I haven't tested.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM