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 selectedToppingOptions.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 theAddRemainingFields
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.