vivekanon
vivekanon

Reputation: 1823

Microsoft BotFramework (Node SDK) : How to implement a recursive hierarchical menu?

I have hierarchical menu structure that I wish to present through the bot and at each level, present 2 additional options [Previous Menu] & [Exit]. [Previous Menu] triggers the parent level. At the selection of the end node the last level options are to be repeated again.

So let's say the menu structure is this:

What I'm trying to do is this :

User : Menu
Bot : [A] [B][Exit]
User (selects): [A]
Bot : [X] [Y] [Previous Menu] [Exit]
User : [X]
Bot : <Dialog for X>
Bot : [X] [Y] [Previous Menu] [Exit]
User : [Y]
Bot : [P] [Q] [Previous Menu] [Exit]
User : [Previous Menu]
Bot : [X] [Y] [Previous Menu] [Exit]
User : [Previous Menu]
Bot : [A] [B][Exit]
User [B]
Bot : [U][V][Previous Menu] [Exit]
User : [U]
Bot : [R][S][Previous Menu] [Exit]

... and so on.

What is the preferred design pattern to implement this behaviour ?

I have an implementation but that relies on having the dialog stack have just one dialog in stack at any moment and replace according to user choices. This can get un manageable as the features grow. Please suggest a decent way to do this reliably.

Thanks

Upvotes: 0

Views: 180

Answers (1)

Xeno-D
Xeno-D

Reputation: 2213

I simplified a previous implementation of mine to give you an example. This may not be the best way to do it but it's simple and can be edited in a lot of ways.

example image

Rootdialog.

This is started from the controller. By typing "menu" the menu will be started.

public class RootDialog : IDialog<object>
{
    private MenuList _menu;

    public async Task StartAsync(IDialogContext context)
    {
        var menuY = new MenuList("Y");
        menuY.AddItem(new MenuItem("P"));
        menuY.AddItem(new MenuItem("Q"));

        var menuA = new MenuList("A");
        menuA.AddItem(new MenuItem("X"));
        menuA.AddItem(menuY);

        var menuU = new MenuList("U");
        menuU.AddItem(new MenuItem("R"));
        menuU.AddItem(new MenuItem("S"));

        var menuB = new MenuList("B");
        menuB.AddItem(menuU);
        menuB.AddItem(new MenuItem("V"));

        _menu = new MenuList("Main");
        _menu.AddItem(menuA);
        _menu.AddItem(menuB);

        context.Wait(MessageReceived);
    }

    private async Task MessageReceived(IDialogContext context, IAwaitable<IMessageActivity> result)
    {
        var message = await result;
        if (message.Text.ToLower() == "menu")
        {
            context.Call(new MenuDialog(_menu), ResumeAfterMenu);
        }
        else
        {
            await context.PostAsync("No result.");
        }
    }

    private async Task ResumeAfterMenu(IDialogContext context, IAwaitable<IMenuItem> result)
    {
        var menuComponent = await result;
        if (menuComponent == null)
        {
            await context.PostAsync("Menu exited.");
            return;
        }
        if (menuComponent is MenuList)
        {
            context.Call(new MenuDialog((MenuList) menuComponent), ResumeAfterMenu);
        }
        else
        {
            var item = (MenuItem) menuComponent;
            await item.Handle(context);
            context.Wait(MessageReceived);
        }
    }

MenuDialog

This class handles the menu choices.

[Serializable]
public class MenuDialog : IDialog<IMenuItem>
{
    private MenuList _menuList;

    public MenuDialog(MenuList list)
    {
        this._menuList = list;
    }

    public async Task StartAsync(IDialogContext context)
    {
        await PromptUser(context);
    }

    private async Task PromptUser(IDialogContext context)
    {
        List<string> optionList = new List<string>();
        optionList.AddRange(_menuList.MenuItems.Select(m => m.name));
        optionList.Add("Back");
        optionList.Add("Exit");
        PromptOptions<string> options = new PromptOptions<string>("Choose an item:", 
            "That's not a valid option", options: optionList, attempts: 5);
        PromptDialog.Choice(context, ReturnSelection, options);
    }

    protected virtual async Task ReturnSelection(IDialogContext context, IAwaitable<string> input)
    {
        string choice;
        try
        {
            choice = await input;
        }
        catch (TooManyAttemptsException)
        {
            context.Done<IMenuItem>(null);
            return;
        }

        if (choice == "Exit")
        {
            context.Done<IMenuItem>(null);
        }
        else if (choice == "Back")
        {
            context.Done<IMenuItem>(_menuList.parent);
        }
        else
        {
            context.Done<IMenuItem>(_menuList.MenuItems.Find(m => m.name == choice));
        }
    }

}

Menu

These are the models I used to construct my menu. The Handle-method first implemented a delegate but I left that out for simplification.

public enum MenuItemType
{
    List, End
}

public interface IMenuItem
{
    string name { get; }
    MenuItemType Type { get; }
    IMenuItem parent { get; set; }
}

[Serializable]
public class MenuItem : IMenuItem
{
    public string name { get; }
    public MenuItemType Type => MenuItemType.End;
    public IMenuItem parent { get; set; }

    public MenuItem(string name)
    {
        this.name = name;
    }

    public async Task Handle(IDialogContext context)
    {
        await context.PostAsync("You activated item " + name);
    }
}

[Serializable]
public class MenuList : IMenuItem
{
    public string name { get; }
    public MenuItemType Type => MenuItemType.List;
    public IMenuItem parent { get; set; }

    public List<IMenuItem> MenuItems { get; }

    public MenuList(string name)
    {
        this.name = name;
        MenuItems = new List<IMenuItem>();
    }

    public void AddItem(IMenuItem item)
    {
        item.parent = this;
        MenuItems.Add(item);
    }
}

Upvotes: 3

Related Questions