I am using bot framework and I am trying to define a form dynamically using FormFlow. I have a problem with one specific field:
.Field(new FieldReflector<IssueFormModel>(nameof(IssueResultModel.ProductPlatform))
.SetType(typeof(string))
.SetDefine(async (issue, field) =>
{
if (issue.ProductName != null)
{
foreach (var platform in productsWithPlatforms[issue.ProductName])
{
field.AddDescription(platform, platform).AddTerms(platform, platform);
}
}
return await Task.FromResult(true);
})
.SetPrompt(new PromptAttribute("Which platform of {ProductName}{||}?"))
.SetAllowsMultiple(false)
.SetValidate(async (state, value) => await ValidateProductPlatform(value, state)))
The problem is that ProductPlatform depends on ProductName, therefore it is a string. This works fine, however, this way, the bot does not show the options of possible platforms (although there is {||} in SetPrompt).
When I set type to null SetType(null)
, the bot now shows possible platforms as buttons, however, it never goes to ValidateProductPlatform when user decides to type a wrong platform instead of clicking on the correct one (I guess the validation itself is done already on SetDefine level). The only reason I need to validate user input through ValidateProductPlatform is that I want to cancel the form after 3 unsuccessful attempts.
So, is there any way to achieve this?: User has options (as buttons) for ProductPlaftorm based on ProductName, but instead of clicking, they (might) type a wrong platform, and after 3 wrong attempts, the form ends.
PS: I saw Microsoft Bot : How to capture Too Many Attempts in Form Flow? but I am not able to use it since it appears as if SetValidate was ignored in my case (when SetType(null)
)
One technique to create a prompter that utilizes both buttons and a custom validation method (on text entry and button press) is to insert a prompter override on your form. This will look something like the following block of code for your particular use case (this implementation overrides the original prompt on the ProductPlatform field in order to add a menu of buttons to the card):
form.Prompter(async (context, prompt, state, field) =>
{
//this part is added in on top of the original implementation of form.Prompter
if (field.Name == nameof(IssueFormModel.ProductPlatform) && prompt.Buttons.Count == 0)
{
foreach (var fieldValue in field.Values)
{
var asString = fieldValue as string;
prompt.Buttons.Add(new DescribeAttribute(asString, title: asString));
}
}
//this check prevents prompts that are sent through from ValidateResult without a FeedbackCard from crashing the bot.
if (prompt.Description != null) {
var preamble = context.MakeMessage();
var promptMessage = context.MakeMessage();
if (prompt.GenerateMessages(preamble, promptMessage))
{
await context.PostAsync(preamble);
}
await context.PostAsync(promptMessage);
}
return prompt;
});
This means the whole of the method in which you build your Form should look something like this:
private IForm<IssueFormModel> BuildProductForm()
{
var form = new FormBuilder<IssueFormModel>()
.Field(new FieldReflector<IssueFormModel>(nameof(IssueFormModel.ProductName))
.SetPrompt(new PromptAttribute("Type either product1 or product2"))
.SetValidate(async (state, value) => await ValidateProductName(value, state)))
.Field(new FieldReflector<IssueFormModel>(nameof(IssueFormModel.ProductPlatform))
.SetType(typeof(string))
.SetDefine(async (issue, field) =>
{
if (issue.ProductName != null)
{
foreach (var platform in productsWithPlatforms[issue.ProductName])
{
field.AddDescription(platform, platform).AddTerms(platform, platform);
}
}
return await Task.FromResult(true);
})
.SetPrompt(new PromptAttribute("Which platform of {ProductName}{||}?"))
.SetAllowsMultiple(false)
.SetValidate(async (state, value) => await ValidateProductPlatform(value, state)))
.AddRemainingFields()
.Confirm(prompt: "Is this your issue? {*}{||}");
form.Prompter(async (context, prompt, state, field) =>
{
if (field.Name == nameof(IssueFormModel.ProductPlatform) && prompt.Buttons.Count == 0)
{
foreach (var fieldValue in field.Values)
{
var asString = fieldValue as string;
prompt.Buttons.Add(new DescribeAttribute(asString, title: asString));
}
}
if (prompt.Description != null) {
var preamble = context.MakeMessage();
var promptMessage = context.MakeMessage();
if (prompt.GenerateMessages(preamble, promptMessage))
{
await context.PostAsync(preamble);
}
await context.PostAsync(promptMessage);
}
return prompt;
});
return form.Build();
}
The main changes here are inserting the form.Prompter after the last call on the FormBuilder and not returning the new form immediately. The overridden prompt uses a type "string" which calls your custom validation, and you should be able to do what you'd like with unsuccessful entries among whatever else there.