0
votes

I have an enum that I am displaying to user for selection. Once user selects any option user should be asked to provide value for the selected option. Once value is provided another confirmation should come asking if user wants to provide more selections. If yes then same enum dialog/prompt should be displayed as earlier. If no then next operation should continue.

I have got the enum dialog and User can make selection as well but now I dont know what should be the approach to request for value and then where to save it and then prompt for confirmation to continue with displaying again the enum dialog.

    public enum OrderSearchOptions
{
    [Describe(Description = "Item Number")]
    [Prompt("Please provide {&}?")]
    ItemNumber,
    [Describe(Description = "Location")]
    Location,
    [Describe(Description = "Country")]
    Issuer,
    [Describe(Description = "Include Breakable")]
    IncludeBreakable,
    Status,
    [Describe(Description = "Packaging Requirement")]
}
[Serializable]
public class OrderAdvanceStepSearchQuery
{
    [Prompt("Please choose search attribute to start the search operation {||}")]
    public OrderSearchOptions? SearchOptions { get; set; }
}
public async Task StartAsync(IDialogContext context)
{
        await context.PostAsync($"Welcome to the Order helper!");
        var OrderFormDialog = FormDialog.FromForm(BuildOrderAdvanceStepSearchForm, FormOptions.PromptInStart);
        context.Call(OrderFormDialog, ResumeAfterOrdersFormDialog);
}   
private IForm<OrderAdvanceStepSearchQuery> BuildOrderAdvanceStepSearchForm()
{
    return new FormBuilder<OrderAdvanceStepSearchQuery>()
            .Build();
}
private async Task ResumeAfterRequiredDWPsFormDialog(IDialogContext context, IAwaitable<OrderAdvanceStepSearchQuery> result)
    {
        try
        {
            var searchQuery = await result;

            await context.PostAsync($"Ok. Searching for Orders...");

            var count = 100;

            if (count > 1)
            {
                await context.PostAsync($"I found total of 100 Orders");

                await context.PostAsync($"To get Order details, you will need to provide more info...");

            }
            else
            {
                await context.PostAsync($"I found the Order you were looking for...");
            }
        }
        catch (FormCanceledException ex)
        {
            string reply;

            if (ex.InnerException == null)
            {
                reply = "You have canceled the operation. Quitting from the Required DWP Search";
            }
            else
            {
                reply = $"Oops! Something went wrong :( Technical Details: {ex.InnerException.Message}";
            }

            await context.PostAsync(reply);
        }
        finally
        {
            context.Done<object>(null);
        }
    }

Following code using IDialog approach.

         public enum OrderSearchOptions
    {
        [Describe(Description = "Item Number")]
        [Prompt("Please provide {&}?")]
        ItemNumber,
        [Describe(Description = "Location")]
        Location,
        [Describe(Description = "Country")]
        Issuer,
        [Describe(Description = "Include Breakable")]
        IncludeBreakable,
        Status,
        [Describe(Description = "Packaging Requirement")]
    }   

    public enum Confirmation
    {
        Yes,
        No
    }

    public async Task StartAsync(IDialogContext context)
    {
        await context.PostAsync($"Welcome to the Order  helper!");

        PromptDialog.Choice(
           context: context,
           resume: ResumeAfterOrderSearchAttributeSelection,
           options: Enum.GetValues(typeof(OrderSearchOptions)).Cast<OrderSearchOptions>().ToArray(),
           prompt: "Please select any Order search attribute to start the search operation:",
           retry: "I didn't understand. Please try again.");
    }

    private async Task ResumeAfterOrderSearchAttributeSelection(IDialogContext context, IAwaitable<OrderSearchOptions> result)
    {
        var message = await result;

        await context.PostAsync("Please provide value for: " + message.ToString());

        context.Wait(OrderSearchAttributeValueReceived);
    }

    private async Task OrderSearchAttributeValueReceived(IDialogContext context, IAwaitable<object> result)
    {
        var message = await result;

        await context.PostAsync("Please provide value as: " + ((Activity)message).Text);

        PromptDialog.Choice(
           context: context,
           resume: ResumeAfterMoreOrderSearchAttributeConfirmation,
           options: Enum.GetValues(typeof(Confirmation)).Cast<Confirmation>().ToArray(),
           prompt: "Do you want to search with more attributes:",
           retry: "I didn't understand. Please try again.");
    }

            private async Task ResumeAfterMoreOrderSearchAttributeConfirmation(IDialogContext context, IAwaitable<Confirmation> result)
    {
        var message = await result;

        if (message == Confirmation.Yes)
        {
            PromptDialog.Choice(
               context: context,
               resume: ResumeAfterOrderSearchAttributeSelection,
               options: Enum.GetValues(typeof(OrderSearchOptions)).Cast<OrderSearchOptions>().ToArray(),
               prompt: "Please select any Order  search attribute to start the search operation:",
               retry: "I didn't understand. Please try again.");
        }
        else
        {
            await context.PostAsync($"Ok. Searching for Order s...");

            var count = 100;

            if (count > 1)
            {
                await context.PostAsync($"I found total of 100 Order s");

                await context.PostAsync($"To get Order  details, you will need to provide more info...");

            }
            else
            {
                await context.PostAsync($"I found the Order  you were looking for...");

                await context.PostAsync($"Now I can provide you information related to your Order .");
            }
        }
    }

Edit: Explanation about Step approach in FormFlow

I understand the Skip approach in the FormFlow but there user was asked to provide information sequentially so i wanted a mechanism that after imp fields we take user confirmation if he/she wants to provide more info. I am good with that approach and I understand it as well. But now I am trying to see the possibility to implement a different approach. Steps:

  1. Bot sends options(enum/class) to User.
  2. User selects an option which is sent to Bot.
  3. Bot receives that options and asks user to provide its value.
  4. Once value is received (where to save this?) Bot ask user if he/she would like to make another selection.
  5. If answer is "Yes" then step 1 is repeated, If answer is "No" then move to next step.
  6. Send all the options:value combinations to DB to complete the search operation.

I am trying to see a possibility to do this via FormFlow and IDialog option as well.

A better IDialog approach but has 2 issue.

  • I am unable to get the proper names of the Enum options like OrderNumber should have been displayed as Order Number.
  • Whatever options user provide needs to be added to a class so that it can be passed to DB/service. I am not getting any idea on how to do that.

Note: I am trying to see the possibilities with FormFlow and IDialog that user decides for which attributes he/she needs to provide information.

    using System;
using System.Threading.Tasks;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Connector;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Bot.Builder.FormFlow;

namespace Bot.Dialogs
{
    [Serializable]
    public class OrderDialog : IDialog<object>
    {    
        public async Task StartAsync(IDialogContext context)
        {
            await context.PostAsync($"Welcome to the Order  helper!");

            await DisplayOrderSearchAttributeSelection(context);
        }

        private async Task DisplayOrderSearchAttributeSelection(IDialogContext context)
        {   
            PromptDialog.Choice(
                context: context,
                resume: ResumeAfterOrderSearchAttributeSelection,
                options: Enum.GetValues(typeof(OrderSearchOptions)).Cast<OrderSearchOptions>().ToArray(),
                prompt: "Please select any Order  search attribute to start the search operation:",
                retry: "I didn't understand. Please try again.");
        }

        private async Task ResumeAfterOrderSearchAttributeSelection(IDialogContext context, IAwaitable<OrderSearchOptions> result)
        {
            var message = await result;

            if (message.ToString().ToLower() == "status")
            {
                await DisplayOrderStatusCreatedAttributeSelection(context);
            }
            else
            {
                await context.PostAsync("Please provide value for: " + message.ToString());

                context.Wait(OrderSearchAttributeValueReceived);
            }
        }

        private async Task OrderSearchAttributeValueReceived(IDialogContext context, IAwaitable<object> result)
        {
            var message = await result as Activity;

            //set received value in the corresponding property.

            await MoreOrderSearchAttributeConfirmation(context);
        }

        private async Task MoreOrderSearchAttributeConfirmation(IDialogContext context)
        {
            PromptDialog.Choice(
                context: context,
                resume: ResumeAfterMoreOrderSearchAttributeConfirmation,
                options: Enum.GetValues(typeof(Confirmation)).Cast<Confirmation>().ToArray(),
                prompt: "Do you want to search with more attributes:",
                retry: "I didn't understand. Please try again.");
        }

        private async Task ResumeAfterMoreOrderSearchAttributeConfirmation(IDialogContext context, IAwaitable<Confirmation> result)
        {
            var message = await result;

            if (message == Confirmation.Yes)
            {
                await DisplayOrderSearchAttributeSelection(context);
            }
            else
            {
                //Take all the values provided by user and send to DB/Service for further processing.

                await context.PostAsync($"Ok. Searching for Order s...");

                var count = 100;

                if (count > 1)
                {
                    await context.PostAsync($"I found total of 100 Order s");

                    await context.PostAsync($"To get Order  details, you will need to provide more info...");

                    //some more logic needs to be written here

                }
                else
                {
                    await context.PostAsync($"I found the Order  you were looking for...");

                    await context.PostAsync($"Now I can provide you information related to Consumer Package, Multi-Pack, Shelf Tray & Unit Load for this Order .");
                }
            }
        }

        private async Task DisplayOrderStatusCreatedAttributeSelection(IDialogContext context)
        {
            PromptDialog.Choice(
                context: context,
                resume: ResumeAfterOrderStatusCreatedAttributeConfirmation,
                options: Enum.GetValues(typeof(Confirmation)).Cast<Confirmation>().ToArray(),
                prompt: "Do you want to search for Status Created?",
                retry: "I didn't understand. Please try again.");
        }

        private async Task ResumeAfterOrderStatusCreatedAttributeConfirmation(IDialogContext context, IAwaitable<Confirmation> result)
        {
            var message = await result;

            //set received value in the corresponding property.

            PromptDialog.Choice(
                context: context,
                resume: ResumeAfterOrderStatusPreliminaryAttributeConfirmation,
                options: Enum.GetValues(typeof(Confirmation)).Cast<Confirmation>().ToArray(),
                prompt: "Do you want to search for Status Preliminary?",
                retry: "I didn't understand. Please try again.");
        }

        private async Task ResumeAfterOrderStatusPreliminaryAttributeConfirmation(IDialogContext context, IAwaitable<Confirmation> result)
        {
            var message = await result;

            //set received value in the corresponding property.

            PromptDialog.Choice(
                context: context,
                resume: ResumeAfterOrderStatusCompletedAttributeConfirmation,
                options: Enum.GetValues(typeof(Confirmation)).Cast<Confirmation>().ToArray(),
                prompt: "Do you want to search for Status Completed?",
                retry: "I didn't understand. Please try again.");
        }

        private async Task ResumeAfterOrderStatusCompletedAttributeConfirmation(IDialogContext context, IAwaitable<Confirmation> result)
        {
            var message = await result;

            //set received value in the corresponding property.

            PromptDialog.Choice(
                context: context,
                resume: ResumeAfterOrderStatusSearchAttributesDone,
                options: Enum.GetValues(typeof(Confirmation)).Cast<Confirmation>().ToArray(),
                prompt: "Do you want to search for Status Expired?",
                retry: "I didn't understand. Please try again.");
        }

        private async Task ResumeAfterOrderStatusSearchAttributesDone(IDialogContext context, IAwaitable<Confirmation> result)
        {
            var searchQuery = await result;

            await MoreOrderSearchAttributeConfirmation(context);
        }

    }
}

public enum OrderSearchOptions
{
    [Describe(Description = "Order Number")]
    OrderNumber,
    [Describe(Description = "Location")]
    Location,
    Issuer,
    [Describe(Description = "Include Breakable")]
    IncludeBreakable,
    Status,
    [Describe(Description = "Packaging Requirement")]
    PackagingRequirement
}

public enum OrderStatus
{
    [Describe(Description = "Status Created")]
    Created,
    Preliminary,
    Completed,
    Expired
}

Edit: Added images for reference

enter image description hereenter image description hereenter image description hereenter image description hereenter image description here

1
When your logic begins to get too complicated, you might find that a normal IDialog<T> rather than FormFlow would be an easier solution. - Joe Mayo
I can try with IDialog as well but I am stuck @ 2 things. When user selects an attribute for search I will prompt him/her to provide values for that. Once the value is provided where & how to save it and then again prompt if user wants to continue or perform search. So its a kind of recursion that is needed if user wants to provide more inputs. I am not able to grab how should I plan that. - niklodeon
I'm confused, do you want to renew the formflow to require user input after prompting a confirmation? Or you just to ask more detailed info to user still in the same formflow after confirmation? - Grace Feng
I have updated the code using IDialog approach. There are two challenges that I see the enum I use does not use Description or Prompt attributes while displaying the options. Like enum has ItemNumber but it should be displayed as "Item Number". Which is not happening. Also once user inputs the value where to save it which relation to the enum property. Hope this is understandable. - niklodeon

1 Answers

1
votes

Once the value is provided where & how to save it and then again prompt if user wants to continue or perform search.

To get the value, you can implement this in your ResumeAfterOrdersFormDialog since you call your FormFlow like this:

context.Call(OrderFormDialog, ResumeAfterOrdersFormDialog);

You can code for example like this:

private async Task ResumeAfterordersFormDialog(IDialogContext context, IAwaitable<object> result)
{
    var searchQuery = await result as OrderSearchQuery;
    var itemnumber = searchQuery.ItemNumber;
    var draft = searchQuery.Draft;

    context.Wait(ResumeAfterordersFormDialog);
}

For saving the data, you may refer to Manage state data. It depends on where do you want to save the data. Just for example here, I save data in table of azure storage:

private async Task ResumeAfterordersFormDialog(IDialogContext context, IAwaitable<object> result)
{
    var searchQuery = await result as OrderSearchQuery;
    var itemnumber = searchQuery.ItemNumber;
    var draft = searchQuery.Draft;

    // Save the information;
    CloudStorageAccount storageAccount = CloudStorageAccount.Parse(CloudConfigurationManager.GetSetting("StorageConnectionString"));
    CloudTableClient tableclient = storageAccount.CreateCloudTableClient();
    CloudTable table = tableclient.GetTableReference("SearchQuery");
    table.CreateIfNotExists();

    MyEntity entity = new MyEntity(itemnumber.ToString(), draft.ToString())
    {
        PartitionKey = itemnumber.ToString(),
        RowKey = DateTime.UtcNow.ToString("yyyyMMddhhmmss")
    };
    TableOperation insertOperation = TableOperation.Insert(entity);
    table.Execute(insertOperation);

    context.Wait(ResumeAfterordersFormDialog);
}

public class MyEntity : TableEntity
{
    public MyEntity(string item, string draft)
    {
        this.Item = item;
        this.Draft = draft;
    }

    public string Item { get; set; }
    public string Draft { get; set; }
}

By the way, you'll need to add StorageConnectionString which is got from azure storage in the Web.config file.

There are two challenges that I see the enum I use does not use Description or Prompt attributes while displaying the options. Like enum has ItemNumber but it should be displayed as "Item Number". Which is not happening.

To display "Item Number" for enum ItemNumber, you can for example code like this:

public enum Item
{
    Item1,
    Item2,
    Item3,
    Item4,
}
public Item? ItemNumber { get; set; }

And it will be rendered like this:

enter image description here

Finally, if you just want to show more detailed information to user in FormFlow dialog after confirmation, the solution will be the same as your last case: Skip displaying form fields based on user confirmation.

By the way, if you use normal IDialog instead of FormFlow dialog, the code for getting and saving data can be moved into MessageReceivedAsync task.

Any further concern, please feel free to let me know.

Update:

Since user selection is dynamic I was not sure how to set the value to corresponding class property.

If you have different steps based on user's selections on the first step and you want to achieve it with FormFlow, you can try to SetActive for the steps. For example:

[Serializable]
public class OrderSearchQuery
{
    public enum SearchAttribute
    {
        OrderNumber,
        Location,
        Issuer,
        IncludeBreakable,
        Status,
        PackagingRequirement
    }

    public SearchAttribute? OrderSearchAttribute { get; set; }

    public string OrderNumber { get; set; }
    public string Location { get; set; }
    public string Issuer { get; set; }
    //And so on...

    private string SelectedItemName;

    private bool AvtivityConverter(string seletedItemname, string currentItemname)
    {
        return seletedItemname == currentItemname;
    }

    public IForm<OrderSearchQuery> BuildOrderAdvanceStepSearchForm()
    {
        return new FormBuilder<OrderSearchQuery>()
            .Field(new FieldReflector<OrderSearchQuery>(nameof(OrderSearchAttribute))
                .SetNext((value, state) =>
                {
                    var selection = (SearchAttribute)value;
                    SelectedItemName = selection.ToString();
                    return new NextStep();
                }))
             .Field(new FieldReflector<OrderSearchQuery>(nameof(OrderNumber))
                 .SetActive(state => AvtivityConverter(SelectedItemName, "OrderNumber")))
             .Field(new FieldReflector<OrderSearchQuery>(nameof(Location))
                 .SetActive(state => AvtivityConverter(SelectedItemName, "Location")))
             .Field(new FieldReflector<OrderSearchQuery>(nameof(Issuer))
                 .SetActive(state => AvtivityConverter(SelectedItemName, "Issuer")))
             //and so on...
             .Build();
    }
}