Friday, May 6, 2016

Dynamic FormFlow Forms in Bot Builder

In a previous post on Bot Builder FormFlow, I described how to automatically construct a dialog with just a class, properties, and a few other methods. The demo was a bit rigid because I used enums to define choices. In practice, you won’t always know ahead of time what choices will be available or the choices could change over time. So, you need a data-driven approach. This post shows how to dynamically define those choices and still have the benefits of FormFlow. This is nearly identical to my previous FormFlow post, except for a couple items that I’ll describe in this post.

Specifying the Dynamic Property

The previous FormFlow example defined the Product property as a Product enum. This is a good candidate for a dynamic data-driven property because over time a bug report will need to accommodate all of the products you support. Here’s the updated BugReport class.

    [Serializable]
    public class BugReport
    {
        public string Product { get; set; }

        public List<PlatformOptions> Platform { get; set; }

        public string ProblemDescription { get; set; }
    }

Here you can see the Product is a string so that it can hold the value that the user enters. The following method simulates a data-driven approach to obtaining the values that can be assigned to Product.

        static List<string> GetProducts()
        {
            return new List<string>
            {
                "Office",
                "SQL Server",
                "Visual Studio"
            };
        }

The GetProducts method returns a collection of strings that can be assigned to the Product property. Now, let’s see how to use these strings in a FormFlow dialog.

Adding a Dynamic Field

In this demo, we’ll modify the BuildForm method, which is a member of the BugReport class. In the previous example, the BuildForm implementation was pretty simple by showing a message, specifying an OnCompletionAsync handler, and initiating the dialog. The difference in this demo is that the code specifically directs which fields/properties to display. Here’s the changed BuildForm method.

        public static IForm<BugReport> BuildForm()
        {
            return new FormBuilder<BugReport>()
                    .Message("Welcome to Bug Report bot!")
                    .Field(new FieldReflector<BugReport>(nameof(Product))
                            .SetType(null)
                            .SetDefine((state, field) =>
                            {
                                foreach (var prod in GetProducts())
                                    field
                                        .AddDescription(prod, prod)
                                        .AddTerms(prod, prod);

                                return Task.FromResult(true);
                            }))
                    .Field(nameof(Platform))
                    .AddRemainingFields()
                    .OnCompletionAsync(async (context, bugReport) => 
                     {
                        await context.PostAsync("Thanks for the report!");
                     })
                    .Build();
        }

In this code, the Field methods describe exactly which properties to display and in what order. FormFlow uses reflection, which the FieldReflector class helps with. The SetDefine method dynamically defines the field. It uses the GetProducts method, shown earlier, and iterates through the list of strings. This is just a demo, so AddDescription and AddTerms use the same string as the parameter key and value. If this was a real bot, GetProducts would read from a database table and return a custom class that contained all of the meta-data required to populate description and terms with more useful values.

Since FormBuilder is a fluent interface, you can continue specifying fields to add and optionally use AddRemainingFields for FormFlow to work normally on any following fields. The rest of the program works the same ways as my previous post on FormFlow.

Summary

This post explained why the default FormFlow wasn’t flexible enough for all scenarios. I defined a string type property and a method that returned all the values that could be assigned to that property. Then you saw how to modify the BuildForm method to specify fields and dynamically define a field with custom data. Now, you’re able to make a bot more adaptable to changes with dynamic fields.

The code for this post is available via my BotDemos GitHub repository.

@JoeMayo

7 comments:

  1. Unfortunately this is also not flexible enough, as i can't use previously filled by user data inside SetDefine(state is not runtime, but defining-time).
    In my app i wanted to generate choices by some external autocomplete service. When user typed something on previous step and i can't parse unique value from it, so i asking external service to give me possible choices.
    I had to split implementation in two different forms, and it looks ugly. Do you know any other options?

    ReplyDelete
    Replies
    1. I'm using separate forms, like you seem to be doing, if my next step depends on the previous response. Chain might be an option too, but I haven't used it in this scenario yet.

      Delete
    2. I'm using separate forms, like you seem to be doing, if my next step depends on the previous response. Chain might be an option too, but I haven't used it in this scenario yet.

      Delete
  2. Hi! I'm wondering if it's possible to set a default or "preloaded" value on the fields of the form, to avoid the user to fill them. I did it setting them into the class's constructor, but I wanted to get them from the user's input.
    Thank you in advance.

    ReplyDelete