Search code examples
botframeworkazure-bot-service

How do I add multiple ComponentDialogs?


I made a new ComponentDialog by creating a class for it that extends ComponentDialog like so:

public class GetPersonInfoDialog : ComponentDialog
{
        protected readonly ILogger Logger;

        public GetPersonInfoDialog(IConfiguration configuration, ILogger<GetPersonInfoDialog> logger)
            : base("get-person-info", configuration["ConnectionName"])
        { }
        // code ommitted
    }
}

Then I added it in Startup.cs:

public void ConfigureServices(IServiceCollection services)
    {

        // ...

        services.AddSingleton<GreetingDialog>();
        services.AddTransient<IBot, AuthBot<GreetingDialog>>();

        // My new component dialog:
        services.AddSingleton<GetPersonInfoDialog>();
        services.AddTransient<IBot, AuthBot<GetPersonInfoDialog>>();
    }
}

But what I noticed is that only the last Dialog gets used. GreetingDialog now no longer works, saying:

DialogContext.BeginDialogAsync(): A dialog with an id of 'greeting' wasn't found. The dialog must be included in the current or parent DialogSet. For example, if subclassing a ComponentDialog you can call AddDialog() within your constructor.

I did notice, however, that the GetPersonInfo dialog does indeed begin and the Greeting dialog doesn't anymore. It's like I can only use one or the other. I noticed only the last added "transient" gets used, as if it overrides the previous transients.

How do I add multiple component dialogs in my Startup.cs file? Or am I even going about this correctly? I did not find any documentation explaining how to have multiple ComponentDialogs.


Solution

  • In Startup.cs

    I will start with your Startup.cs file since that is where the first issue is, then I will suggest an alternative design.

    What you are effectively doing with the following block of code is:

    services.AddSingleton<GreetingDialog>();
    services.AddTransient<IBot, AuthBot<GreetingDialog>>();
    
    services.AddSingleton<GetPersonInfoDialog>();
    services.AddTransient<IBot, AuthBot<GetPersonInfoDialog>>();
    
    1. Registering a single instance of the GreetingDialog for your bot (essentially a static).
    2. Registering the IBot interface to return a new AuthBot of type GreetingDialog everytime an IBot is requested.
    3. Registering a single instance of the GetPersonInfoDialog for your bot (essentially a static).
    4. Registering the IBot interface (again) to return a new AuthBot of type GetPersonInfoDialog everytime an IBot is requested (which will overwrite the registration in step 2).

    You can read a lot more about Service lifetimes here.

    So what you actually want is more like the below:

    public void ConfigureServices(IServiceCollection services)
    {
        // Other code
    
        // Register dialogs
        services.AddTransient<GreetingDialog>();
        services.AddTransient<GetPersonInfoDialog>();
    
        // Some more code
    
        // Configure bot
        services.AddTransient<IBot, DialogBot<GreetingDialog>>();
    }
    

    Error message

    DialogContext.BeginDialogAsync(): A dialog with an id of 'greeting' wasn't found. The dialog must be included in the current or parent DialogSet. For example, if subclassing a ComponentDialog you can call AddDialog() within your constructor.

    This error message is caused because your GetPersonInfoDialog doesn't know about your GreetingDialog (and it shouldn't). I believe this is a runtime error because I remember running into a similar issue myself. Since you haven't provided the full implementation for your GetPersonInfoDialog class I have to assume that somewhere inside there you are trying to do something like the following:

    dialogContext.BeginDialogAsync("greeting");
    
    or
    
    dialogContext.BeginDialogAsync(nameof(GreetingDialog));
    

    as per the documentation the first parameter is the ID of the dialog to start, this ID is also used to retrieve the dialog from the dialog stack. In order to call one dialog from inside another, you will need to add it to the parent dialog's DialogSet. The accepted way to do this is to add a call inside the constructor for the parent dialog like so:

    public ParentDialog(....)
        : base(nameof(ParentDialog)
    {
        // Some code
    
        // Important part
        AddDialog(new ChildDialog(nameof(ChildDialog)));
    }
    

    This uses the AddDialog method provided by the Microsoft.Bot.Builder.Dialogs NuGet package and exposed through the ComponentDialog class.

    Then when you want to display the ChildDialog you would call:

    dialogContext.BeginDialogAsync(nameof(ChildDialog));
    

    In your case you can replace ParentDialog with GetPersonInfoDialog and ChildDialog with GreetingDialog. Since your GreetingDialog is only likely to be used once (it is not a utility dialog that could be called multiple times but with different arguments - in this case you would want to provide a specific ID rather than using nameof(GreetingDialog)) it is fine to go with the string representation of the class name as the DialogId, you can using "greeting" inside the the AddDialog call but you would also have to update the BeginDialogAsync call to also use "greeting".


    An alternative design

    Since I don't believe you want either GreetingDialog or GetPersonInfoDialog to be your actual starting points, I would suggest adding another dialog called MainDialog which inherits from the RouterDialog class (Microsoft.Bot.Builder.Solutions.Dialogs NuGet package). Based on the architecture (Virtual Assistant Template) here you would have your MainDialog spawn off your GreetingDialog and GetPersonInfoDialog.

    Assuming that your GreetingDialog is only a single stage where it sends a card or some text to the user to welcome them, it could be completely replaced by the OnStartAsync method which sends your card/message. Getting your user to your GetPersonInfoDialog would then be handled by the RouteAsync method example here.

    The changes you would need to make to your existing project to get this wired up are (assuming that you keep the GreetingDialog):

    • Add Transient registrations in Startup.cs for GreetingDialog, GetPersonInfoDialog and MainDialog.
    • Add a Transient registration for mapping IBot to AuthBot<MainDialog>
    • Add calls inside the constructor of MainDialog to add the child dialogs GreetingDialog and GetPersonInfoDialog.
    • In the OnBeginDialog or OnStartAsync of MainDialog start your GreetingDialog.
    • In the RouteAsync of MainDialog handle any conditions around showing the GetPersonInfoDialog before displaying it.
    • There may be some additional steps that I have missed.

    Helpful links:


    Edit

    To achieve what you want within the OAuth sample you could do the following:

    In LogoutDialog.cs change:

    private async Task<DialogTurnResult> InterruptAsync(DialogContext innerDc, CancellationToken cancellationToken = default(CancellationToken))
    

    to

    protected virtual async Task<DialogTurnResult> InterruptAsync(DialogContext innerDc, CancellationToken cancellationToken = default(CancellationToken))
    

    In MainDialog.cs add:

    protected override async Task<DialogTurnResult> InterruptAsync(DialogContext innerDc, CancellationToken cancellationToken = default(CancellationToken))
    {
        if (innerDc.Context.Activity.Type == ActivityTypes.Message)
        {
            var text = innerDc.Context.Activity.Text.ToLowerInvariant();
    
            if (text == "check email")
            {
                //
                return innerDc.BeginDialogAsync(/*TODO*/);
            }
            else if (text == "check calender")
            {
                //
                return innerDc.BeginDialogAsync(/*TODO*/);
            }
            // etc
    
    
            return await base.InterruptAsync(innerDc, cancellationToken);
        }
    
        return null;
    }
    

    along with registering your Calendar, Email, etc dialogs in the constructor for MainDialog using the AddDialog method.

    I would seriously advise you to look at using the Virtual Assistant Template. As it uses LUIS to determing the user's intent (check email, check calendar etc), then route them accordingly, the relevant code is in this method. Using LUIS to determine intents has the advantage of being able to tie multiple ways of asking the same thing to the same intent, so you're not relying on your users to explicitly type "check calendar", you can have "show me my calendar", "what is my availability for next Monday", "am I free this afternoon", "check if I have any appointments tomorrow" etc. In fact Microsoft has already built Skills for email, and calendar which work with the Virtual Assistant Template, it should be easy enough to port the login code over to this template.