CHAPTER 7. Customizing FormFlow

FormFlow offers a quick way to build conversations and Chapter 6, Using FormFlow, explained the essentials for how FormFlow works. For all the great standard features that FormFlow offers, in practical use, you’ll want additional customizations. This chapter moves beyond the basics and explains a few customization options.

One of the customizations you’ll need is to build dynamic menus, rather than relying on a static enum. In this chapter you’ll learn how to modify the form build process to dynamically build a menu for a field. You’ll also learn how to add custom validation to that field. Another customization is a reuse strategy and you’ll learn how to build a custom field type. Finally, a useful technique to make the user’s experience better is to pre-populate fields.

This chapter uses the WineForm concept from Chapter 6, with customizations. We’ll focus on the customizations in this chapter (but see Chapter 6 for more information on how WineBot works if needed). The first customization in this chapter uses the FormFlow fluent interface.

Understanding the FormFlow Fluent Interface

A fluent interface is one where the return value of one class member is the instance used to call another method. Visually, this starts by instantiating a class, following the class instantiation with a dot operator, calling another method, and continuing with the dot method pattern, eventually ending in a method that returns a type you want to operate on. The Chapter 6 listings for WineForm used the FormFlow fluent interface like the code shown here:

        public IForm<WineForm> BuildForm()
        {
            return new FormBuilder<WineForm>()
                .Message(
                    “I have a few questions on your wine search. “ +
                    “You can type ”help” at any time for more info.”)
                .OnCompletion(DoSearch)
                .Build();
        }

This is the BuildForm method from Chapter 6, Listing 6-4, but you don’t need to go back and look at that to understand what is covered here. To summarize, the previous code example shows that the FormBuilder<T> type has Message, OnCompletion, and Build methods. FormBuilder<T> implements IFormBuilder<T> and Message, OnCompletion, and Build return IFormBuilder<T> instances, supporting the ability to build a FormFlow form with a fluent interface. Here’s part of the IFormBuilder<T> definition, showing these and more members:

    public interface IFormBuilder<T>
        where T : class
    {
        IForm<T> Build(
            Assembly resourceAssembly = null,
            string resourceName = null);

        FormConfiguration Configuration { get; }

        IFormBuilder<T> Message(
            string message, 
            ActiveDelegate<T> condition = null, 
            IEnumerable<string> dependencies = null);

        IFormBuilder<T> Field(IField<T> field);

        IFormBuilder<T> AddRemainingFields(
            IEnumerable<string> exclude = null);

        IFormBuilder<T> Confirm(
            string prompt = null, 
            ActiveDelegate<T> condition = null, 
            IEnumerable<string> dependencies = null);

        IFormBuilder<T> OnCompletion(
            OnCompletionAsyncDelegate<T> callback);

        bool HasField(string name);
    }

All of the methods in IFormBuilder<T> return IFormBuilder<T>, except for Build and HasField, which return IForm<T> and bool, respectively. Some of the methods have multiple overloads, not shown, but the following sections discuss overloads and explain how each IFormBuilder<T> member works.

The Configuration Property

As explained in Chapter 6, FormFlow has a set of defaults it uses in three areas: commands, responses, and templates. If those defaults don’t meet your needs, you can change them through the Configuration property as shown in Listing 7-1.

Listing 7-1 Customizing FormFlow Configuration

using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.FormFlow;
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Bot.Builder.Resource;
using WineBotLib;

namespace WineBotConfiguration
{
    [Serializable]
    public class WineForm
    {
        public WineType WineType { get; set; }
        public RatingType Rating { get; set; }
        public StockingType InStock { get; set; }

        public IForm<WineForm> BuildForm()
        {
            var builder = new FormBuilder<WineForm>();
            ConfigureFormBuilder(builder);

            return builder
                .Message(
                    “I have a few questions on your wine search. “ +
                    “You can type ”help” at any time for more info.”)
                .OnCompletion(DoSearch)
                .Build();
        }

        void ConfigureFormBuilder(FormBuilder<WineForm> builder)
        {
            FormConfiguration buildConfig = builder.Configuration;

            buildConfig.Yes = “Yes;y;sure;ok;yep;1;good”.SplitList();

            TemplateAttribute tmplAttr = buildConfig.Template(TemplateUsage.EnumSelectOne);
            tmplAttr.Patterns = new[] {“What {&} would you like? {||}”};

            buildConfig.Commands[FormCommand.Quit].Help =
                “Quit: Quit the form without completing it. “ +
                “Warning - this will clear your previous choices!”;
        }

        async Task DoSearch(IDialogContext context, WineForm wineInfo)
        {
            List[] wines =
                await new WineApi().SearchAsync(
                    (int)WineType,
                    (int)Rating,
                    InStock == StockingType.InStock,
                    “”);

            string message;

            if (wines.Any())
                message = “Here are the top matching wines: “ +
                          string.Join(“, “, wines.Select(w => w.Name));
            else
                message = “Sorry, No wines found matching your criteria.”;

            await context.PostAsync(message);
        }
    }
}

The BuildForm method in Listing 7-1 is a little different than previous examples because it doesn’t continue calling methods off the new FormBuilder<WineForm>() instance. It assigns the new instance to builder and then passes builder as an argument to the ConfigureFormBuilder method.


Images Tip

IFormBuilder<T> methods rely on default configuration to operate and then consider attributes to override the defaults. Because of this, remember to perform configuration to change the defaults before calling any methods.


Inside ConfigureFormBuilder, the code assigns a reference to the FormConfiguration instance of the Configuration property. The following sections show the three types of configuration actions you can take to customize FormFlow Configuration.

Configuring Responses

Having accepted an IFormBuilder<WineForm> instance, builder, the ConfigureFormBuilder method in Listing 7-1 shows how to customize the three areas of Configuration. The first example, repeated next, shows how to modify FormConfiguration responses:

buildConfig.Yes = “Yes;y;sure;ok;yep;1;good”.SplitList();

This particular response configuration is for the Yes property, which is a string[] of potential responses to any situation where FormFlow expects an answer and one of the answers could be Yes. The SplitList method is a Bot Builder extension method that creates an array, using semi-colons as the default separator, but with overloads allowing you to change to your preferred separator. The following summarization of FormConfiguration shows the response properties you can customize:

    public class FormConfiguration
    {
        /// <summary>
        /// Enumeration of strings for interpreting a user response as setting an optional field to be unspecified.
        /// </summary>
        /// <remarks>
        /// The first string is also used to describe not having a preference for an optional field.
        /// </remarks>
        public string[] NoPreference = Resources.MatchNoPreference.SplitList();

        /// <summary>
        /// Enumeration of strings for interpreting a user response as asking for the current value.
        /// </summary>
        /// <remarks>
        /// The first value is also used to describe the option of keeping the current value.
        /// </remarks>
        public string[] CurrentChoice = Resources.MatchCurrentChoice.SplitList();

        /// <summary>
        /// Enumeration of values for a “yes” response for boolean fields or confirmations.
        /// </summary>
        public string[] Yes = Resources.MatchYes.SplitList();

        /// <summary>
        /// Enumeration of values for a “no” response for boolean fields or confirmations.
        /// </summary>
        public string[] No = Resources.MatchNo.SplitList();

        /// <summary>
        /// String for naming the “navigation” field.
        /// </summary>
        public string Navigation = Resources.Navigation;

        /// <summary>
        /// String for naming “Confirmation” fields.
        /// </summary>
        public string Confirmation = Resources.Confirmation;
    };

In each property, you can see that FormFlow uses a Bot Builder resource file to set strings. Similarly, you can define your own .NET resource file to localize possible responses to common questions.

Configuring Templates

The next item you can customize is templates. If you recall, from Chapter 6, the Template attribute allows you to override the default prompt for anything that FormFlow prints out. Through Configuration, you can change these defaults, as shown by the excerpt from Listing 7-1, repeated below:

TemplateAttribute tmplAttr = buildConfig.Template(TemplateUsage.EnumSelectOne);
tmplAttr.Patterns = new[] {“What {&} would you like? {||}”};

FormConfiguration has a method named Template, accepting a TemplateUsage argument, which is the same TemplateUsage discussed in Chapter 6 for the Template attribute. This returns a TemplateAttribute instance that you can use to customize the defaults for that particular template. This example replaces the default string[] for the Patterns used to select a prompt to display to the user. The following Templates field shows the available FormConfiguration defaults:

    public class FormConfiguration
    {
        public List<TemplateAttribute> Templates = new List<TemplateAttribute>
        {
            new TemplateAttribute(TemplateUsage.Bool, Resources.TemplateBool),
            // {0} is current choice, {1} is no preference
            new TemplateAttribute(TemplateUsage.BoolHelp, Resources.TemplateBoolHelp),

            // {0} is term being clarified
            new TemplateAttribute(TemplateUsage.Clarify, Resources.TemplateClarify),

            new TemplateAttribute(TemplateUsage.Confirmation, Resources.TemplateConfirmation),

            new TemplateAttribute(TemplateUsage.CurrentChoice, Resources.TemplateCurrentChoice),

            new TemplateAttribute(TemplateUsage.DateTime, Resources.TemplateDateTime),
            // {0} is current choice, {1} is no preference
            // new TemplateAttribute(TemplateUsage.DateTimeHelp, 
			    “Please enter a date or time expression like ‘Monday’ or ‘July 3rd’{?, {0}}{?, {1}}.”),
            new TemplateAttribute(TemplateUsage.DateTimeHelp, Resources.TemplateDateTimeHelp),

            // {0} is min and {1} is max.
            new TemplateAttribute(TemplateUsage.Double, Resources.TemplateDouble) 
			    { ChoiceFormat = Resources.TemplateDoubleChoiceFormat },
            // {0} is current choice, {1} is no preference
            // {2} is min and {3} is max
            new TemplateAttribute(TemplateUsage.DoubleHelp, Resources.TemplateDoubleHelp),

            // {0} is min, {1} is max and {2} are enumerated descriptions
            new TemplateAttribute(TemplateUsage.EnumManyNumberHelp, Resources.TemplateEnumManyNumberHelp),
            new TemplateAttribute(TemplateUsage.EnumOneNumberHelp, Resources.TemplateEnumOneNumberHelp),

            // {2} are the words people can type
            new TemplateAttribute(TemplateUsage.EnumManyWordHelp, Resources.TemplateEnumManyWordHelp),
            new TemplateAttribute(TemplateUsage.EnumOneWordHelp, Resources.TemplateEnumOneWordHelp),

            new TemplateAttribute(TemplateUsage.EnumSelectOne, Resources.TemplateEnumSelectOne),
            new TemplateAttribute(TemplateUsage.EnumSelectMany, Resources.TemplateEnumSelectMany),

            // {0} is the not understood term
            new TemplateAttribute(TemplateUsage.Feedback, Resources.TemplateFeedback),

            // For {0} is recognizer help and {1} is command help.
            new TemplateAttribute(TemplateUsage.Help, Resources.TemplateHelp),
            new TemplateAttribute(TemplateUsage.HelpClarify, Resources.TemplateHelpClarify),
            new TemplateAttribute(TemplateUsage.HelpConfirm, Resources.TemplateHelpConfirm),
            new TemplateAttribute(TemplateUsage.HelpNavigation, Resources.TemplateHelpNavigation),

            // {0} is min and {1} is max if present
            new TemplateAttribute(TemplateUsage.Integer, Resources.TemplateInteger) 
			    { ChoiceFormat = Resources.TemplateIntegerChoiceFormat },
            // {0} is current choice, {1} is no preference
            // {2} is min and {3} is max
            new TemplateAttribute(TemplateUsage.IntegerHelp, Resources.TemplateIntegerHelp),

            new TemplateAttribute(TemplateUsage.Navigation, Resources.TemplateNavigation) 
			    { FieldCase = CaseNormalization.None },
            // {0} is list of field names.
            new TemplateAttribute(TemplateUsage.NavigationCommandHelp, Resources.TemplateNavigationCommandHelp),
            new TemplateAttribute(TemplateUsage.NavigationFormat, Resources.TemplateNavigationFormat) 
			    {FieldCase = CaseNormalization.None },
            // {0} is min, {1} is max
            new TemplateAttribute(TemplateUsage.NavigationHelp, Resources.TemplateNavigationHelp),

            new TemplateAttribute(TemplateUsage.NoPreference, Resources.TemplateNoPreference),

            // {0} is the term that is not understood
            new TemplateAttribute(TemplateUsage.NotUnderstood, Resources.TemplateNotUnderstood),

            new TemplateAttribute(TemplateUsage.StatusFormat, Resources.TemplateStatusFormat) 
			    {FieldCase = CaseNormalization.None },

            new TemplateAttribute(TemplateUsage.String, Resources.TemplateString) 
			    { ChoiceFormat = Resources.TemplateStringChoiceFormat },
            // {0} is current choice, {1} is no preference
            new TemplateAttribute(TemplateUsage.StringHelp, Resources.TemplateStringHelp),

            new TemplateAttribute(TemplateUsage.Unspecified, Resources.TemplateUnspecified)
        };
    }

Each TemplateAttribute constructor accepts arguments of type TemplateUsage and string[]. As with the response properties, you can assign localizable resources to the patterns argument.

Similar to TemplateAttribute, FormConfiguration has a PromptAttribute that you can replace to set default prompt settings, shown here:

    public class FormConfiguration
    {
        public PromptAttribute DefaultPrompt = new PromptAttribute(“”)
        {
            AllowDefault = BoolDefault.True,
            ChoiceCase = CaseNormalization.None,
            ChoiceFormat = Resources.DefaultChoiceFormat,
            ChoiceLastSeparator = Resources.DefaultChoiceLastSeparator,
            ChoiceParens = BoolDefault.True,
            ChoiceSeparator = Resources.DefaultChoiceSeparator,
            ChoiceStyle = ChoiceStyleOptions.Auto,
            FieldCase = CaseNormalization.Lower,
            Feedback = FeedbackOptions.Auto,
            LastSeparator = Resources.DefaultLastSeparator,
            Separator = Resources.DefaultSeparator,
            ValueCase = CaseNormalization.InitialUpper
        };
    }

PromptAttribute inherits all of those properties from its abstract base class, TemplateBaseAttribute. You can also set those properties through a TemplateAttribute instance, which also derives from TemplateBaseAttribute.

Configuring Commands

The last Configuration category is commands. A command is a way to communicate with FormFlow itself, rather than the form class, like WineForm, that the developer creates. As Chapter 6 discusses, these commands include Back, Status, Help, and more. The following code shows how to customize the Help command:

            buildConfig.Commands[FormCommand.Quit].Help =
                “Quit: Quit the form without completing it. “ +
                “Warning - this will clear your previous choices!”;

This sets the Help message for the Quit command, which as you saw in Chapter 6, will appear where the user types Help. In the help message is a Quit option and this sets the message that appears.

The Commands property is a Dictionary<FormCommand, CommandDescription> where FormCommand is an enum with a specific set of commands that FormFlow supports and CommandDescription, shown here:

    public class CommandDescription
    {
        /// <summary>
        /// Description of the command.
        /// </summary>
        public string Description;

        /// <summary>
        /// Regexs for matching the command.
        /// </summary>
        public string[] Terms;

        /// <summary>
        /// Help string for the command.
        /// </summary>
        public string Help;

        /// <summary>
        /// Construct the description of a built-in command.
        /// </summary>
        /// <param name=”description”>Description of the command.</param>
        /// <param name=”terms”>Terms that match the command.</param>
        /// <param name=”help”>Help on what the command does.</param>
        public CommandDescription(string description, string[] terms, string help)
        {
            Description = description;
            Terms = terms;
            Help = help;
        }
    }

As you can see, CommandDescription has Description, Terms, and Help fields that can be set directly, or you can replace the entire dictionary value for that command with a new CommandDescription instance. Notice that the only fields available customize the appearance of an existing command. You can’t add arbitrary commands this way, but can customize any that are members of the FormCommand enum, shown below:

    public enum FormCommand
    {
        /// <summary>
        /// Move back to the previous step.
        /// </summary>
        Backup,

        /// <summary>
        /// Ask for help on responding to the current field.
        /// </summary>
        Help,

        /// <summary>
        /// Quit filling in the current form and return failure to parent dialog.
        /// </summary>
        Quit,

        /// <summary>
        /// Reset the status of the form dialog.
        /// </summary>
        Reset,

        /// <summary>
        /// Provide feedback to the user on the current form state.
        /// </summary>
        Status
    };

Images Tip

Besides just organizing code to separate and clarify custom configuration logic, the ConfigureFormBuilder method implies that you might want to move this logic into another method and/or class to reuse and set defaults with common code for multiple forms in a chatbot.


After optionally configuring the form, you can call methods and the next section eases into that by discussing common parameters for Message and other methods.

The Message Method and Common Parameters

The Message method supports sending any type of non-question related information to the user. It has overloads that accept a varying list of parameters that either permit building the message text a specific way or controlling the conditions of whether or not the Message method will display text. Listing 7-2, from the WineFormParams project in the accompanying source code, shows the different Message overloads.

LISTING 7-2 Using the Message method

using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.FormFlow;
using System;
using System.Linq;
using System.Threading.Tasks;
using WineBotLib;

namespace WineBotParams
{
    [Serializable]
    public class WineForm
    {
        public WineType WineType { get; set; }
        [Optional]
        public RatingType Rating { get; set; }
        public StockingType InStock { get; set; }

        public IForm<WineForm> BuildForm()
        {
            ActiveDelegate<WineForm> shouldShowSpecial =
                wineForm => DateTime.Now.DayOfWeek == DayOfWeek.Friday;

            var prompt = new PromptAttribute
            {
                Patterns =
                    new[]
                    {
                        “Hi, I have a few questions to ask.”,
                        “How are you today? I just have a few questions.”,
                        “Thanks for visiting - please answer a few questions.”
                    }
            };

            int numberOfBackOrderDays = 15;

            MessageDelegate<WineForm> generateMessage =
                async wineForm => 
                    await Task.FromResult(
                        new PromptAttribute(
                            $”Note: Delivery back order is {numberOfBackOrderDays} days.”));
            
            return new FormBuilder<WineForm>()
                .Message(prompt)
                .Message(
                    “You can type ”help” at any time for more info.”)
                .Message(
                    “It’s your lucky day - 10% discounts on Friday!”,
                    shouldShowSpecial)
                .Message(
                    $”Today you get an additional %5 off.”,
                    wineForm => wineForm.Rating == RatingType.Low,
                    new[] { nameof(Rating) })
                .Message(
                    generateMessage, 
                    wineForm => wineForm.InStock == StockingType.OutOfStock)
                .OnCompletion(DoSearch)
                .Build();
        }

        async Task DoSearch(IDialogContext context, WineForm wineInfo)
        {
            List[] wines =
                await new WineApi().SearchAsync(
                    (int)WineType,
                    (int)Rating,
                    InStock == StockingType.InStock,
                    “”);

            string message;

            if (wines.Any())
                message = “Here are the top matching wines: “ +
                          string.Join(“, “, wines.Select(w => w.Name));
            else
                message = “Sorry, No wines found matching your criteria.”;

            await context.PostAsync(message);
        }
    }
}

As Listing 7-2 shows, the BuildForm method instantiates a new FormBuilder<WineForm> and calls several Message overloads. The second Message overload displays a string, which appears as the first user message when FormFlow starts, before any questions. The following sections discuss other overloads and their associated parameters.

The condition Parameter

The condition parameter determines whether the Message will display. Its parameter conforms to the ActiveDelegate signature, shown here:

public delegate bool ActiveDelegate<T>(T state);

With ActiveDelegate the state type is the FormFlow form, which is WineForm in these examples. The code the developer writes for this delegate instance is logic that returns a bool result. When the return value is true, the message displays, and won’t display if false. The following excerpt from Listing 7-2 shows how this works:

            ActiveDelegate<WineForm> shouldShowSpecial =
                wineForm => DateTime.Now.DayOfWeek == DayOfWeek.Friday;

            return new FormBuilder<WineForm>()
                .Message(
                    “It’s your lucky day - 10% discounts on Friday!”,
                    shouldShowSpecial)
                .Build();

Here, you can see the code assigns a lambda to an ActiveDelegate<WineForm> instance, shouldShowSpecial. The concept here is that the chatbot offers a 10% discount on Fridays. On any other day, the message won’t display.

The dependencies Parameter

Some questions are optional and you need a way to display a message only if the user decides to provide a value. That’s the case with the Rating property in WineForm, as shown here:

        [Optional]
        public RatingType Rating { get; set; }

        public IForm<WineForm> BuildForm()
        {
            return new FormBuilder<WineForm>()
                .Message(
                    $”Today you get an additional %5 off.”,
                    wineForm => wineForm.Rating == RatingType.Low,
                    new[] { nameof(Rating) })
                .OnCompletion(DoSearch)
                .Build();
        }

This excerpt, from Listing 7-2, decorates Rating with an Optional attribute. The Message example in BuildForm accepts three parameters: the message text, the condition (as described in the previous section), and the dependency fields. In particular, if the user doesn’t choose to complete the Rating property, this message won’t dispay. If the user does provide a value, and meets the condition that the selection is a Low rating, then the message displays. This example defines condition as a lambda, instead of an ActivieDelegate<WineForm> delegate reference. The concept here is a potential situation where the chatbot wants to clear inventory that’s moving slow because of low ratings and wants to provide an incentive for the user to buy.


Images Note

WineForm implements properties for its state, but let’s refer to them as fields, which doesn’t conform to C# language syntax guidelines. However, here we refer to both properties and fields as FormFlow fields. Since FormFlow is a multi-language library in Bot Builder, it has its own naming idioms, rather than conforming to what a specific language prescribes. We could use C# fields, which might reduce confusion, but let’s use properties. However, from a terminology standpoint, we’ll cover Field methods later in the chapter and discuss the FormFlow perspective, using the term field, which has merit.


Notice that dependencies is a string[], indicating that there can be dependencies on multiple fields. Dependencies on required fields, without the Optional attribute, prevents the message from appearing.

The point in time of the conversation when a message appears, assuming its matched the dependencies and condition critertia, is after acknowledging the final confirmation of the form. This makes sense because there’s no way to know for sure that the value is set until after the user confirms so. Until final confirmation, the user can navigate at any time and change the field to No Preference.

The prompt Parameter

The Message method has an overload accepting the prompt parameter, which is type PromptAttribute. This is the same Prompt attribute that’s explained in Chapter 6. Here’s an excerpt from Listing 7-2, showing how to define a prompt parameter:

        public IForm<WineForm> BuildForm()
        {
            var prompt = new PromptAttribute
            {
                Patterns =
                    new[]
                    {
                        “Hi, I have a few questions to ask.”,
                        “How are you today? I just have a few questions.”,
                        “Thanks for visiting - please answer a few questions.”
                    }
            };

            return new FormBuilder<WineForm>()
                .Message(prompt)
                .OnCompletion(DoSearch)
                .Build();
        }

Notice that the PromptAttribute Patterns property has an array of multiple messages. This is a handy way to vary a message so the user has some variation across multiple interactions with a chatbot.

The generateMessage Parameter

Another Message overload accepts a generateMessage argument. This is a good idea if you need dynamic logic to build the message. Here’s an excerpt from Listing 7-2, showing how to use generateMessage:

        public IForm<WineForm> BuildForm()
        {
            int numberOfBackOrderDays = 15;

            MessageDelegate<WineForm> generateMessage =
                async wineForm => 
                    await Task.FromResult(
                        new PromptAttribute(
                            $”Note: Delivery back order is {numberOfBackOrderDays} days.”));
            
            return new FormBuilder<WineForm>()
                .Message(
                    generateMessage, 
                    wineForm => wineForm.InStock == StockingType.OutOfStock)
                .OnCompletion(DoSearch)
                .Build();
        }

The generateMessage parameter type is MessageDelegate<WineForm>, which returns a Task type, allowing an async operation. This is useful because, although this example hard codes it, you might want to make an async database call to get the current number of back order days. Combined with the condition for OutOfStock, this message attempts to be helpful and let’s the user know how long they’ll wait for products that aren’t in stock.

All of the parameters for Message are available in Confirm method overloads, which is discussed next.

The Confirm Method

The Confirm method calls for the user to acknowledge a statement. Positive responses, such as saying yes, allow the user to progress with the form and negative responses, such as no, will prevent the user from proceeding in the form. Listing 7-3, from the WineFormConfirm project in the accompanying source code, shows how to use the Confirm method.

LISTING 7-3 Using the Confirm Method

using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.FormFlow;
using System;
using System.Linq;
using System.Threading.Tasks;
using WineBotLib;

namespace WineBotConfirm
{
    [Serializable]
    public class WineForm
    {
        public WineType WineType { get; set; }
        [Optional]
        public RatingType Rating { get; set; }
        public StockingType InStock { get; set; }

        public IForm<WineForm> BuildForm()
        {
            ActiveDelegate<WineForm> shouldShowContest =
                wineForm => DateTime.Now.DayOfWeek == DayOfWeek.Friday;

            var prompt = new PromptAttribute
            {
                Patterns =
                    new[]
                    {
                        “Hi, May I ask a few questions?”,
                        “How are you today? Can I ask a few questions?”,
                        “Thanks for visiting - would you answer a few questions?”
                    }
            };

            int numberOfBackOrderDays = 15;

            MessageDelegate<WineForm> generateMessage =
                async wineForm =>
                    await Task.FromResult(
                        new PromptAttribute(
                            $”Delivery back order is {numberOfBackOrderDays} days. Are you sure?”));

            return new FormBuilder<WineForm>()
                .Confirm(prompt)
                .Confirm(
                    “You can type ”help” at any time for more info. Would you like to proceed?”)
                .Confirm(
                    “Would you like to enter a contest for free bottle of Wine?”,
                    shouldShowContest)
                .Confirm(
                    $”Low rated wines are limited in stock - are you sure?”,
                    wineForm => wineForm.Rating == RatingType.Low,
                    new[] { nameof(Rating) })
                .Confirm(
                    generateMessage,
                    wineForm => wineForm.InStock == StockingType.OutOfStock)
                .OnCompletion(DoSearch)
                .Build();
        }

        async Task DoSearch(IDialogContext context, WineForm wineInfo)
        {
            List[] wines =
                await new WineApi().SearchAsync(
                    (int)WineType,
                    (int)Rating,
                    InStock == StockingType.InStock,
                    “”);

            string message;

            if (wines.Any())
                message = “Here are the top matching wines: “ +
                          string.Join(“, “, wines.Select(w => w.Name));
            else
                message = “Sorry, No wines found matching your criteria.”;

            await context.PostAsync(message);
        }
    }
}

The BuildForm method in Listing 7-3 is similar to the BuildForm method in Listing 7-2, except it uses the Confirm method and different text. The parameters in each of the Confirm method overloads are the same parameters used in the previous section on Message, where you’ll find a more detailed description.


Images Tip

The difference between Confirm and Message methods is that the Message doesn’t require user acknowledgement and always moves forward, but Confirm prevents a user from moving forward until they positively acknowledge the question.


Working with Fields

Until now, you’ve seen examples that rely on public properties as FormFlow fields. By default, FormFlow uses reflection to read these fields in the order they appear in the class. Chapter 6 showed how to decorate these fields with attributes for customization, but that’s limited. In this section, you’ll learn how to take full control of fields with dynamic definition and validation. You’ll also learn how to control which fields appear and in what order. The following sections take a deep dive into Listing 7-4, from the WineBotFields project in the accompanying source code, which shows different ways to work with fields.

LISTING 7-4 WineForm for Working with Fields

using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.FormFlow;
using Microsoft.Bot.Builder.FormFlow.Advanced;
using System;
using System.Linq;
using System.Threading.Tasks;
using WineBotLib;

namespace WineBotFields
{
    [Serializable]
    public class WineForm
    {
        public string WineType { get; set; }
        public int Rating { get; set; }
        public StockingType InStock { get; set; }
        public int Vintage { get; set; }
        public string SearchTerms { get; set; }

        public Refinement[] WineCategories { get; set; }

        public IForm<WineForm> BuildForm()
        {
            var form = new FormBuilder<WineForm>()
                .Message(“Welcome to WineBot!”)
                .Field(nameof(InStock), wineForm => DateTime.Now.DayOfWeek == DayOfWeek.Wednesday)
                .Field(new FieldReflector<WineForm>(nameof(WineType))
                    .SetType(null)
                    .SetFieldDescription(“Type of Wine”)
                    .SetDefine(async (wineForm, field) =>
                    {
                        foreach (var category in WineCategories)
                            field
                                .AddDescription(category.Name, category.Name)
                                .AddTerms(category.Name, Language.GenerateTerms(category.Name, 6));

                        return await Task.FromResult(true);
                    }))
                .Field(
                    name: nameof(Rating),
                    prompt: new PromptAttribute(“What is your preferred {&} (1 to 100)?”),
                    active: wineForm => true,
                    validate: async (wineForm, response) =>
                    {
                        var result = new ValidateResult { IsValid = true, Value = response };

                        result.IsValid =
                            int.TryParse(response.ToString(), out int rating) &&
                            rating > 0 && rating <= 100;

                        if (!result.IsValid)
                        {
                            result.Feedback = $”’{response}’ isn’t a valid option!”;
                            result.Choices =
                                new List<Choice>
                                {
                                    new Choice
                                    {
                                        Description = new DescribeAttribute(“25”),
                                        Value = 25,
                                        Terms = new TermsAttribute(“25”)
                                    },
                                     new Choice
                                    {
                                        Description = new DescribeAttribute(“50”),
                                        Value = 50,
                                        Terms = new TermsAttribute(“50”)
                                    },
                                    new Choice
                                    {
                                        Description = new DescribeAttribute(“75”),
                                        Value = 75,
                                        Terms = new TermsAttribute(“75”)
                                    }
                               };
                        }

                        return await Task.FromResult(result);
                    })
                .AddRemainingFields(new[] { nameof(Vintage) });

            if (!form.HasField(nameof(Vintage)))
                form.Field(nameof(Vintage));

            form.OnCompletion(DoSearch);

            return form.Build();
        }

        async Task DoSearch(IDialogContext context, WineForm wineInfo)
        {
            int wineType =
                (from refinement in WineCategories
                 where refinement.Name == wineInfo.WineType
                 select refinement.Id)
                .SingleOrDefault();

            List[] wines =
                await new WineApi().SearchAsync(
                    wineType,
                    wineInfo.Rating,
                    wineInfo.InStock == StockingType.InStock,
                    wineInfo.SearchTerms);

            string message;

            if (wines.Any())
                message = “Here are the top matching wines: “ +
                          string.Join(“, “, wines.Select(w => w.Name));
            else
                message = “Sorry, No wines found matching your criteria.”;

            await context.PostAsync(message);

            context.EndConversation(EndOfConversationCodes.CompletedSuccessfully);
        }
    }
}

In its simplest form, the Field method has an overload that only requires the name of a field, as in the following excerpt from Listing 7-4:

            var form = new FormBuilder<WineForm>()
                .Message(“Welcome to WineBot!”)
                .Field(nameof(InStock), wineForm => DateTime.Now.DayOfWeek == DayOfWeek.Wednesday);

This Field defines the InStock field. Additionally, it uses an optional active parameter, which is an ActiveDelegate instance, as described in the previous Message method section. Since the default FormFlow behavior is to order properties based on their declared order in the class, this is one way to change the order of fields from the default. In this example, the default ordering is to ask the WineType field question first, because it appears first in the class, but using the Field method makes the InStock field question show first instead.

The first time you use the Field method, it overrides the default FormFlow behavior, requiring explicitly using a Field method for each field. In the following sections, you’ll learn how to dyamically add, exclude, and fill in remaining fields.

Dynamic Field Definition

The Field method has several overloads, one of which let’s you dynamically define the field. This allows you to specify all aspects of a field to include its type, members, underlying values, and any other customizations to define the field. The following excerpt, from Listing 7-4, shows how to dynamically define a field:

            var form = new FormBuilder<WineForm>()
                .Message(“Welcome to WineBot!”)
                .Field(new FieldReflector<WineForm>(nameof(WineType))
                    .SetType(null)
                    .SetFieldDescription(“Type of Wine”)
                    .SetDefine(async (wineForm, field) =>
                    {
                        foreach (var category in WineCategories)
                            field
                                .AddDescription(category.Name, category.Name)
                                .AddTerms(category.Name, Language.GenerateTerms(category.Name, 6));

                        return await Task.FromResult(true);
                    }));

The Field method overload accepts a parameter of type Field<T>. The FieldReflector<WineForm> instance helps build a new Field<WineForm> instance. Like FormBuilder<T>, FieldReflector<T> is the entry point of a fluent interface where each method returns Field<T>. Further, Field<T> has methods, such as the SetType, SetFieldDescription, and SetDefine in the example above, in addition to many more.

To understand Field<T> members, consider all that you’ve learned about fields and their attributes from Chapter 6 and this chapter. Each of the Field<T> members support everything you’ve learned so far.

SetType indicates the field type, except when the value should be treated like an enum and is set to null. By passing null to SetType, the previous example tells FormFlow to treat the WineType field as an enum. In WineForm, the WineType property type is string, which is used to hold the value that the user responds with, and while the type of the property doesn’t need to be an enum, FormFlow gives the user an enum-like experience by showing buttons with the question.

SetFieldDescription is analogous to using a Description attribute on a field. Instead of the constructing the default name Wine Type, FormFlow will use Type of Wine, as specified by the parameter.

SetDefine defines the contents of the question, adding allowable values for the user to choose from. Remember, the whole purpose of using this particular overload is to dynamically define available answers, rather than relying on hard-coded enum values. SetDefine takes an async DefineAsyncDelegate<T>, shown here:

    public delegate Task<bool> DefineAsyncDelegate<T>(T state, Field<T> field)
        where T : class;

For this example, the type T is WineForm and the two parameters, wineForm and field, are type WineForm and Field<WineForm>, respectively.

WineCategories is a Refinement[]–a serializable type returned via repository calls to WineApi and you’ll see how WineCategories gets populated later in this chapter. The call to AddDescription adds possible answers, where the parameters are value and description. AddTerms, both analogous to using the Terms attribute, where the first parameter is the value and the second is string[].


Images Note

We’ve seen a couple methods whose identifiers contain Description. One is SetFieldDescription, which operates the same as a Description attribute decorating a FormFlow property or field. The AddDescription is different because it displays a name for a potential value of a field, instead of the field itself.


Rather than manually specify terms, the example uses the Bot Builder Language.GenerateTerms method to automatically create up to six regular expressions that can possibly match this value. Table 7-1 shows what Language.GenerateTerms returns for each category.

Table 7-1 Language.GenerateTerms Examples

Phrases

Generated Terms

Red Wine

reds?, wines?, reds wines?

White Wine

whites?, wines?, whites? wines?

Champagne & Sparkling

champagnes?, sparklings?, champagnes? & sparklings?

Rosé Wine

rosés?, wines?, rosés? wines?

Dessert, Sherry & Port

dessert,s?, sherrys?, ports?, dessert,s? sherrys?, sherrys? & ports?, dessert,s? sherrys? & ports?

Saké

sakés?

The second parameter to Language.GenerateTerms is set to 6, meaning that it can generate 6 or fewer terms. The generated terms are regular expressions that FormFlow recognizes when validating that user input matches. Table 7-1 shows that longer phrases, like Dessert, Sherry & Port generate up to 6 regular expressions. Shorter phrases don’t have enough words to generate 6 regular expressions. The DefineAsyncDelegate<T> instance returns true if it defined the field.

Field Validation

Field<T> has a SetValidation method you can use to perform validation on the user’s input. You don’t need all of the ceremony of a FieldReflector<T> with multiple method calls, however, because the Field method has an overload taking a validation parameter, as shown in the following excerpt from Listing 7-4:

            var form = new FormBuilder<WineForm>()
                .Message(“Welcome to WineBot!”)
                .Field(
                    name: nameof(Rating),
                    prompt: new PromptAttribute(“What is your preferred {&} (1 to 100)?”),
                    active: wineForm => true,
                    validate: async (wineForm, response) =>
                    {
                        var result = new ValidateResult { IsValid = true, Value = response };

                        result.IsValid =
                            int.TryParse(response.ToString(), out int rating) &&
                            rating > 0 && rating <= 100;

                        result.IsValid =
                            int.TryParse(response.ToString(), out int rating) &&
                            rating > 0 && rating <= 100;

                        if (!result.IsValid)
                        {
                            result.Feedback = $”’{response}’ isn’t a valid option!”;
                            result.Choices =
                                new List<Choice>
                                {
                                    new Choice
                                    {
                                        Description = new DescribeAttribute(“25”),
                                        Value = 25,
                                        Terms = new TermsAttribute(“25”)
                                    },
                                     new Choice
                                    {
                                        Description = new DescribeAttribute(“50”),
                                        Value = 50,
                                        Terms = new TermsAttribute(“50”)
                                    },
                                    new Choice
                                    {
                                        Description = new DescribeAttribute(“75”),
                                        Value = 75,
                                        Terms = new TermsAttribute(“75”)
                                    }
                               };
                        }

                       return await Task.FromResult(result);
                    });

This Field overload has name, prompt, active, and validate parameters. The name is the name of the field to store results in, prompt is analogous to the Prompt attribute on a field, and active is the ActiveDelegate used in previous examples to determine whether FormFlow should ask a question for that field. The validate parameter is a ValidateAsyncDelegate, shown here:

public delegate Task<ValidateResult> ValidateAsyncDelegate<T>(T state, object value);

The first parameter of ValidateAsyncDelegate, in the Field example, is a reference to the current WineForm instance and the second is the user’s response to the question. The validation logic instantiates a ValidateResult, initializing IsValid to true and Value to the user’s response. The validation you use depends on what makes sense for the field. In this case, the validation is to ensure the user’s response is an integer between 1 and 100. The code returns the ValidateResult instance, result.

When the user’s input is not valid, the code populates the result.Feedback property to give the user more information on what went wrong. The result.Choices property displays a list of buttons the user can click. The Choice class has properties for Description, Value, and Terms. The user can read Description, Value is the input sent back to the chatbot when the user clicks the button, and Terms is a list of items that could match that choice. (See the previous discussion on Language.GenerateTerms for ideas on how to populate Terms.) When you populate result.Choices, only the presented choices are valid and the user can’t type any other text. Another way to communicate error details to the user is with the FeedbackCard property, shown here:

        result.FeedbackCard =
            new FormPrompt
            {
                Prompt = $”’{response}’ isn’t a valid option!”,
                Buttons =
                    new List<DescribeAttribute>
                    {
                        new DescribeAttribute(“25”),
                        new DescribeAttribute(“50”),
                        new DescribeAttribute(“75”)
                    }
            };

This is nearly the same as the Feedback and Choice properties, except we use a single FormPrompt instance and the result comes out as a card. The FormPrompt has a Prompt and Buttons properties where the Prompt is the text the user explaining what went wrong. The Buttons property shows a button for each option, using a DescribeAttribute for each option. Unlike the Choice property, FeedbackCard lets the user type any value, regardless of whether it’s specified by a button, making this option more desirable when the buttons aren’t the only values a user could type.

The AddRemainingFields Method

As mentioned previously, calling a single Field method on FormBuilder<T> overrides the default behavior of FormFlow to automatically ask questions for each field. When that happens, FormFlow will only ask questions for fields that are explicitly defined. That poses a problem in that it can be cumbersome to explicitly define every field, so FormBuilder<T> has a method, AddRemainingFields, that does exactly what it sounds like–adds all remaining fields that haven’t been explicitly specified. Here’s an excerpt from Listing 7-4, showing how to use AddRemainingFields:

            var form = new FormBuilder<WineForm>()
                .Message(“Welcome to WineBot!”)
                .Field(nameof(InStock), wineForm => DateTime.Now.DayOfWeek == DayOfWeek.Wednesday)
                .AddRemainingFields(new[] { nameof(Vintage) });

In the previous example, if InStock were the only field specified, it would be the only question that FormFlow asks the user. The AddRemainingFields method includes all of the other fields in the form, except for a string[] with field names to exclude. This example adds all of the fields, except for Vintage. AddRemainingFields has a parameterless overload in case you don’t want to exclude any fields.

The HasField Method

FormBuilder<T> offers a HasField method, allowing queries to determine if a given field is specified in the FormBuilder<T>. Here’s an excerpt, from Listing 7-4, showing one way to use HasField:

            if (!form.HasField(nameof(Vintage)))
                form.Field(nameof(Vintage));

This code checks to see if the FormBuider<T> defines a Vintage field. If not, the code adds the Vintage field.


Images Tip

The HasField example shows the beauty of using a fluent interface. You can use your own logic to dynamically determine whether to add or remove elements from a FormBuilder<T>. The same applies for Field<T>.


The OnCompletion Method

When FormFlow completes asking questions, you’ll want to do something with the results, such as saving in a database, providing an answer, or performing a search. The following excerpt, from Listing 7-4, shows how to use OnCompletion to process those results:

            var form = new FormBuilder<WineForm>()
                .Message(“Welcome to WineBot!”)
                .Field(nameof(InStock), wineForm => DateTime.Now.DayOfWeek == DayOfWeek.Wednesday)
                .AddRemainingFields(new[] { nameof(Vintage) })
                .OnCompletion(DoSearch);

OnCompletion specifies the DoSearch method for processing form results. The DoSearch method implements the OnCompletionAsyncDelegate signature, shown here:

    public delegate Task OnCompletionAsyncDelegate<T>(IDialogContext context, T state);

Type T in the OnCompletionAsyncDelegate that OnCompletion refers to is WineForm and the parameter types are IDialogContext and WineForm. This IDialogContext is the same type described in detail in Chapter 5, Building Dialogs, for IDialog<T> implementation method parameters. Here’s the DoSearch method, showing one way to use these parameters to process results:

        async Task DoSearch(IDialogContext context, WineForm wineInfo)
        {
            int wineType =
                (from refinement in WineCategories
                 where refinement.Name == wineInfo.WineType
                 select refinement.Id)
                .SingleOrDefault();

            List[] wines =
                await new WineApi().SearchAsync(
                    wineType,
                    wineInfo.Rating,
                    wineInfo.InStock == StockingType.InStock,
                    wineInfo.SearchTerms);

            string message;

            if (wines.Any())
                message = “Here are the top matching wines: “ +
                          string.Join(“, “, wines.Select(w => w.Name));
            else
                message = “Sorry, No wines found matching your criteria.”;

            await context.PostAsync(message);

            context.EndConversation(EndOfConversationCodes.CompletedSuccessfully);
        }

The wineInfo parameter is the WineForm instance, containing the user’s responses to FormFlow questions. The first thing DoSearch does is get the category number via the LINQ query on the WineCategories collection. WineCategories is a Refinement[] from the WineApi, containing both the category name and id and WineApi needs a category id for its search.

The call to WineApi.SearchAsync passes the category id, wineType, and the rest of the values it needs from wineInfo. The return value is a List, which is a type from WineApi containing details of wine results. The method creates a message for the user, based on whether the search returned wine.

The PostAsync method, from the IDialogContext instance, context, is the same used many times previously. It sends a message to the user with the search results.

Finally, the method tells Bot Connector that it’s done with this form by calling context.EndConversation. The parameter to EndConversation is a string, named code, to communicate the reason why the form is ending the conversation. This example uses EndOfConversationCodes.CompletedSuccessfully from the following EndOfConversationCodes class to indicate successful completion:

    public class EndOfConversationCodes
    {
        /// <summary>
        /// The conversation was ended for unknown reasons
        /// </summary>
        public const string Unknown = “unknown”;

        /// <summary>
        /// The conversation completed successfully
        /// </summary>
        public const string CompletedSuccessfully = “completedSuccessfully”;

        /// <summary>
        /// The user cancelled the conversation
        /// </summary>
        public const string UserCancelled = “userCancelled”;

        /// <summary>
        /// The conversation was ended because requests sent to the bot timed out
        /// </summary>
        public const string BotTimedOut = “botTimedOut”;

        /// <summary>
        /// The conversation was ended because the bot sent an invalid message
        /// </summary>
        public const string BotIssuedInvalidMessage = “botIssuedInvalidMessage”;

        /// <summary>
        /// The conversation ended because the channel experienced an internal failure
        /// </summary>
        public const string ChannelFailed = “channelFailed”;
    }

The Build Method

After specifying each part of the form, call the Build method, as copied here from Listing 7-4:

            return form.Build();

In this example, form is the FormBuilder<WineForm> instance. The Build method performs all of the internal work to prepare the form, based on how you defined it with FormBuilder<WineForm> methods and wraps the result in an IForm<WineForm> to return from the BuildForm method.

That’s it for how to build a FormFlow form. Now, let’s look at how properties, like WineCategories, get populated and a new way to wrap a FormFlow form in an IDialog<T> for Bot Builder consumption.

Initializing FormFlow

Previous examples used the static FormDialog.FromForm method to wrap a FormFlow form in an IDialog<T>. However, that won’t work for our latest form in Listing 7-4 because this example requires pre-populated fields. Therefore, this section shows another way to get an IDialog<T>, while providing default values for form fields. Listing 7-5 shows the chatbot code that does this.

LISTING 7-5 Initializing a FormFlow Form

using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Connector;
using Microsoft.Bot.Builder.FormFlow;
using System;
using WineBotLib;

namespace WineBotFields
{
    [BotAuthentication]
    public class MessagesController : ApiController
    {
        public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
        {
            if (activity.Type == ActivityTypes.Message)
                try
                {
                    IDialog<WineForm> wineDialog = await BuildWineDialogAsync();
                    await Conversation.SendAsync(activity, () => wineDialog);
                }
                catch (FormCanceledException<WineForm> fcEx)
                {
                    Activity reply = activity.CreateReply(fcEx.Message);
                    var connector = new ConnectorClient(new Uri(activity.ServiceUrl));
                    connector.Conversations.ReplyToActivity(reply);
                }

            return Request.CreateResponse(HttpStatusCode.OK);
        }

        Refinement[] wineCategories;

        async Task<IDialog<WineForm>> BuildWineDialogAsync()
        {
            if (wineCategories == null)
                wineCategories = await new WineApi().GetWineCategoriesAsync();

            var wineForm = new WineForm
            {
                WineCategories =
                    wineCategories,
                InStock = StockingType.InStock,
                Rating = 75,
                Vintage = 2010
            };

            return new FormDialog<WineForm>(
                wineForm, 
                wineForm.BuildForm, 
                FormOptions.PromptFieldsWithValues);
        }
    }
}

In Listing 7-5, the Post method calls BuildWineDialogAsync to get an IDialog<WineForm> for Bot Builder’s Conversation.SendAsync. In previous examples, this was a simple call to FormDialog.FromFrom, but the requirements for this example are different. Essentially, we want to send a list of wine categories, from WineApi, to WineForm.

The code populates wineCategories, which is a Refinement[], from the call to GetWineCategoriesAsync in WineApi. This is the same WineApi code that we’ve been using since Chapter 5, where you can find a detailed description of how it works.

The code instantiates a WineForm, but this isn’t the WineForm instance that FormFlow will use. Instead, its purpose is to initialize fields. Notice that the code also populates WineCategories, which isn’t included as a WineForm field, but is how we pass those categories to the form.


Images Tip

Imagine if you were keeping track of a user’s choices between each visit to WineBot, saving the choices and retrieving those choices on subsequent visits. You can populate a WineForm instance like this example, with those saved values. This can save the user’s time in filling out the form.


Finally, the code instantiates a new FormDialog<WineForm> whose parameters are state, buildForm, and options. The state parameter is the WineForm instance, wineForm. FormDialog populates the fields in the FormFlow form with matching values from the wineForm instance passed as the state parameter, setting new defaults for the user. The buildForm parameter refers to the BuildForm method in WineForm. The default behavior when fields already have values is for FormFlow to ignore the fields with values and only display fields that don’t have values. Using FormOptions.PromptFieldsWithValues forces FormFlow to abandon the default behavior and ask questions for each field, regardless of whether the field has a value. Here’s the FormOptions enum:

    [Flags]
    public enum FormOptions
    {
        /// <summary>
        /// No options.
        /// </summary>
        None,

        /// <summary>
        /// Prompt when the dialog starts.
        /// </summary>
        PromptInStart,

        /// <summary>  
        /// Prompt for fields that already have a value in the initial state when processing form.
        /// </summary>
        PromptFieldsWithValues
    };

The FormOptions parameter, options, defaults to None. Also, notice the Flags attribute, allowing you to combine PromptInStart and PromptFieldsWithValues.

As you’ve seen, the process of initializing a FormFlow form wraps a class inside of FormFlow types, resulting in an object implementing IDialog<T>. In particular, you saw how to pass arguments, which is useful when you have full or partial information and want to hand off the next part of the conversation to a FormFlow form. You’ll learn how perform these hand-offs and navigate between dialogs in Chapter 9, Managing Advanced Conversation.

Summary

This chapter showed several advanced techniques for customizing FormFlow. It started with an overview of the FormBuilder<T> fluent interface and continued with available methods.

You learned how to add messages and confirmations to the form, along with common parameters for customizing their behaviors.

This chapter showed several examples of how to define fields, changing order, dynamic definition, and custom validation. There is also a discussion of how to fill in the rest of the form automatically and determine if a field has been defined.

You also learned how to handle the results of a form when it’s complete.

Finally, the chapter showed you how to set default values before the user starts the form and how to determine whether the user should fill in forms, regardless of whether a field has a value or not.

You now have several tools for dynamically creating and customizing FormFlow forms. The next chapter continues the story of Bot Framework dialogs with LuisDialog, allowing users to interact with a chatbot using natural language.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.205.154