Customize a form using FormBuilder in the v3 C# SDK

APPLIES TO: SDK v3

Basic features of FormFlow describes a basic FormFlow implementation that delivers a fairly generic user experience, and Advanced features of FormFlow describes how you can customize user experience by using business logic and attributes. This article describes how you can use FormBuilder to customize user experience even further, by specifying the sequence in which the form executes steps and dynamically defining field values, confirmations, and messages.

Dynamically define field values, confirmations, and messages

Using FormBuilder, you can dynamically define field values, confirmations, and messages.

Dynamically define field values

A sandwich bot that is designed to add a free drink or cookie to any order that specifies a foot-long sandwich uses the Sandwich.Specials field to store data about free items. In this case, the value of the Sandwich.Specials field must be dynamically set for each order according to whether or not the order contains a foot-long sandwich.

The Specials field is specified as optional and "None" is designated as text for the choice that indicates no preference.

[Optional]
[Template(TemplateUsage.NoPreference, "None")]
public string Specials;

This code example shows how to dynamically set the value of the Specials field.

.Field(new FieldReflector<SandwichOrder>(nameof(Specials))
    .SetType(null)
    .SetActive((state) => state.Length == LengthOptions.FootLong)
    .SetDefine(async (state, field) =>
    {
        field
            .AddDescription("cookie", "Free cookie")
            .AddTerms("cookie", "cookie", "free cookie")
            .AddDescription("drink", "Free large drink")
            .AddTerms("drink", "drink", "free drink");
        return true;
    }))

In this example, the Advanced.Field.SetType method specifies the field type (null represents an enumeration field). The Advanced.Field.SetActive method specifies that the field should only be enabled if the length of the sandwich is Length.FootLong. Finally, the Advanced.Field.SetDefine method specifies an async delegate that defines the field. The delegate is passed the current state object and the Advanced.Field that is being dynamically defined. The delegate uses the field's fluent methods to dynamically define values. In this example, the values are strings and the AddDescription and AddTerms methods specify the descriptions and terms for each value.

Note

To dynamically define a field value, you can implement Advanced.IField yourself, or streamline the process by using the Advanced.FieldReflector class as shown in the example above.

Dynamically define messages and confirmations

Using FormBuilder, you can also dynamically define messages and confirmations. Each message and confirmation runs only when prior steps in the form are inactive or completed.

This code example shows a dynamically generated confirmation that computes the cost of the sandwich.

.Confirm(async (state) =>
{
    var cost = 0.0;
    switch (state.Length)
    {
        case LengthOptions.SixInch: cost = 5.0; break;
        case LengthOptions.FootLong: cost = 6.50; break;
    }
    return new PromptAttribute($"Total for your sandwich is {cost:C2} is that ok?");
})

Customize a form using FormBuilder

This code example uses FormBuilder to define the steps of the form, validate selections, and dynamically define a field value and confirmation. By default, steps in the form will be executed in the sequence in which they are listed. However, steps might be skipped for fields that already contain values or if explicit navigation is specified.

public static IForm<SandwichOrder> BuildForm()
{
    OnCompletionAsyncDelegate<SandwichOrder> processOrder = async (context, state) =>
    {
        await context.PostAsync("We are currently processing your sandwich. We will message you the status.");
    };

    return new FormBuilder<SandwichOrder>()
        .Message("Welcome to the sandwich order bot!")
        .Field(nameof(Sandwich))
        .Field(nameof(Length))
        .Field(nameof(Bread))
        .Field(nameof(Cheese))
        .Field(nameof(Toppings),
            validate: async (state, value) =>
            {
                var values = ((List<object>)value).OfType<ToppingOptions>();
                var result = new ValidateResult { IsValid = true, Value = values };
                if (values != null && values.Contains(ToppingOptions.Everything))
                {
                    result.Value = (from ToppingOptions topping in Enum.GetValues(typeof(ToppingOptions))
                                    where topping != ToppingOptions.Everything && !values.Contains(topping)
                                    select topping).ToList();
                }
                return result;
            })
        .Message("For sandwich toppings you have selected {Toppings}.")
        .Field(nameof(SandwichOrder.Sauces))
        .Field(new FieldReflector<SandwichOrder>(nameof(Specials))
            .SetType(null)
            .SetActive((state) => state.Length == LengthOptions.FootLong)
            .SetDefine(async (state, field) =>
            {
                field
                    .AddDescription("cookie", "Free cookie")
                    .AddTerms("cookie", "cookie", "free cookie")
                    .AddDescription("drink", "Free large drink")
                    .AddTerms("drink", "drink", "free drink");
                return true;
            }))
        .Confirm(async (state) =>
        {
            var cost = 0.0;
            switch (state.Length)
            {
                case LengthOptions.SixInch: cost = 5.0; break;
                case LengthOptions.FootLong: cost = 6.50; break;
            }
            return new PromptAttribute($"Total for your sandwich is {cost:C2} is that ok?");
        })
        .Field(nameof(SandwichOrder.DeliveryAddress),
            validate: async (state, response) =>
            {
                var result = new ValidateResult { IsValid = true, Value = response };
                var address = (response as string).Trim();
                if (address.Length > 0 && (address[0] < '0' || address[0] > '9'))
                {
                    result.Feedback = "Address must start with a number.";
                    result.IsValid = false;
                }
                return result;
            })
        .Field(nameof(SandwichOrder.DeliveryTime), "What time do you want your sandwich delivered? {||}")
        .Confirm("Do you want to order your {Length} {Sandwich} on {Bread} {&Bread} with {[{Cheese} {Toppings} {Sauces}]} to be sent to {DeliveryAddress} {?at {DeliveryTime:t}}?")
        .AddRemainingFields()
        .Message("Thanks for ordering a sandwich!")
        .OnCompletion(processOrder)
        .Build();
}

In this example, the form executes these steps:

  • Shows a welcome message.
  • Fills in SandwichOrder.Sandwich.
  • Fills in SandwichOrder.Length.
  • Fills in SandwichOrder.Bread.
  • Fills in SandwichOrder.Cheese.
  • Fills in SandwichOrder.Toppings and adds missing values if the user selected ToppingOptions.Everything. -. Shows a message that confirms the selected toppings.
  • Fills in SandwichOrder.Sauces.
  • Dynamically defines the field value for SandwichOrder.Specials.
  • Dynamically defines the confirmation for cost of the sandwich.
  • Fills in SandwichOrder.DeliveryAddress and verifies the resulting string. If the address does not start with a number, the form returns a message.
  • Fills in SandwichOrder.DeliveryTime with a custom prompt.
  • Confirms the order.
  • Adds any remaining fields that were defined in the class but not explicitly referenced by Field. (If the example did not call the AddRemainingFields method, the form would not include any fields that were not explicity referenced.)
  • Shows a thank you message.
  • Defines an OnCompletionAsync handler to process the order.

Sample code

For complete samples that show how to implement FormFlow using the Bot Framework SDK for .NET, see the Multi-Dialog Bot sample and the Contoso Flowers Bot sample in GitHub.

Additional resources