I'm trying to get the result of my AdaptiveCard.
My bot uses waterfalldialogs. In one Waterfallstep i present the user a number of Rooms with the time and date. The user then can choose a room. I tried it like shown below. Sadly the activity stays null. How do I get the result of the adaptive card
private async Task<DialogTurnResult> AfterChoice(WaterfallStepContext step, CancellationToken cancellationToken)
{
if (step.Result is Activity activity && activity.Value != null && ((dynamic)activity.Value).chosenRoom is JValue chosenRoom)
{
dynamic requestedBooking = JsonConvert.DeserializeObject<ExpandoObject>((string)chosenRoom.Value);
this.roomemail = requestedBooking.roomEmail;
return await step.EndDialogAsync();
}
else
{
return await step.BeginDialogAsync(whatever, cancellationToken: cancellationToken);
}
}
How do I get the users choice?
Adaptive Cards send their Submit results a little different than regular user text. When a user types in the chat and sends a normal message, it ends up in Context.Activity.Text. When a user fills out an input on an Adaptive Card, it ends up in Context.Activity.Value, which is an object where the key names are the id in your card and the values are the field values in the adaptive card.
For example, the json:
{
"type": "AdaptiveCard",
"body": [
{
"type": "TextBlock",
"text": "Test Adaptive Card"
},
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"items": [
{
"type": "TextBlock",
"text": "Text:"
}
],
"width": 20
},
{
"type": "Column",
"items": [
{
"type": "Input.Text",
"id": "userText",
"placeholder": "Enter Some Text"
}
],
"width": 80
}
]
}
],
"actions": [
{
"type": "Action.Submit",
"title": "Submit"
}
],
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.0"
}
.. creates a card that looks like:
If a user enters "Testing Testing 123" in the text box and hits Submit, Context.Activity will look something like:
{ type: 'message',
value: { userText: 'Testing Testing 123' },
from: { id: 'xxxxxxxx-05d4-478a-9daa-9b18c79bb66b', name: 'User' },
locale: '',
channelData: { postback: true },
channelId: 'emulator',
conversation: { id: 'xxxxxxxx-182b-11e9-be61-091ac0e3a4ac|livechat' },
id: 'xxxxxxxx-182b-11e9-ad8e-63b45e3ebfa7',
localTimestamp: 2019-01-14T18:39:21.000Z,
recipient: { id: '1', name: 'Bot', role: 'bot' },
timestamp: 2019-01-14T18:39:21.773Z,
serviceUrl: 'http://localhost:58453' }
The user submission can be seen in Context.Activity.Value.userText.
Note that adaptive card submissions are sent as a postBack, which means that the submission data doesn't appear in the chat window as part of the conversation--it stays on the Adaptive Card.
Using Adaptive Cards with Waterfall Dialogs
Your question doesn't quite relate to this, but since you may end up attempting this, I thought it might be important to include in my answer.
Natively, Adaptive Cards don't work like prompts. With a prompt, the prompt will display and wait for user input before continuing. But with Adaptive Cards (even if it contains an input box and a submit button), there is no code in an Adaptive Card that will cause a Waterfall Dialog to wait for user input before continuing the dialog.
So, if you're using an Adaptive Card that takes user input, you generally want to handle whatever the user submits outside of the context of a Waterfall Dialog.
That being said, if you want to use an Adaptive Card as part of a Waterfall Dialog, there is a workaround. Basically, you:
Display the Adaptive Card
Display a Text Prompt
Convert the user's Adaptive Card input into the input of a Text Prompt
In your Waterfall Dialog class (steps 1 and 2):
private async Task<DialogTurnResult> DisplayCardAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
// Create the Adaptive Card
var cardPath = Path.Combine(".", "AdaptiveCard.json");
var cardJson = File.ReadAllText(cardPath);
var cardAttachment = new Attachment()
{
ContentType = "application/vnd.microsoft.card.adaptive",
Content = JsonConvert.DeserializeObject(cardJson),
};
// Create the text prompt
var opts = new PromptOptions
{
Prompt = new Activity
{
Attachments = new List<Attachment>() { cardAttachment },
Type = ActivityTypes.Message,
Text = "waiting for user input...", // You can comment this out if you don't want to display any text. Still works.
}
};
// Display a Text Prompt and wait for input
return await stepContext.PromptAsync(nameof(TextPrompt), opts);
}
private async Task<DialogTurnResult> HandleResponseAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
// Do something with step.result
// Adaptive Card submissions are objects, so you likely need to JObject.Parse(step.result)
await stepContext.Context.SendActivityAsync($"INPUT: {stepContext.Result}");
return await stepContext.NextAsync();
}
In your main bot class (<your-bot>.cs) (step 3):
var activity = turnContext.Activity;
if (string.IsNullOrWhiteSpace(activity.Text) && activity.Value != null)
{
activity.Text = JsonConvert.SerializeObject(activity.Value);
}
Related
In Adaptive Card, it's easy to create a submit button:
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.0",
"body": [
{
"type": "TextBlock",
"text": "Present a form and submit it back to the originator"
},
{
"type": "Input.Text",
"id": "firstName",
"placeholder": "What is your first name?"
},
{
"type": "Input.Text",
"id": "lastName",
"placeholder": "What is your last name?"
}
],
"actions": [
{
"type": "Action.Submit",
"title": "Action.Submit",
"data": {
"x": 13
}
}
]
}
Which look like this:
Is it possible to also use Toggle Visibility as defined here?
https://adaptivecardsci.z5.web.core.windows.net/pr/3261/explorer/Action.ToggleVisibility.html
So what I owuld like to achieve is when the user click the button, it will both submit the form and also make the form element invisible.
Emulator does not support message updates or deletions, so you will not be able to test this functionality in Emulator. However, you can still debug your bot locally on a channel like Teams using a tunneling service like ngrok: https://blog.botframework.com/2017/10/19/debug-channel-locally-using-ngrok/
You can find examples of how to update an acitivity in the following answers:
Can we add text field dynamically
Microsoft teams bot adaptive card carousel deleting a card
Simple example to store array data in card
You can see that it involves bot state. If you just want to delete the whole activity then your job may be easier because you won't need to save any information about the activities except for the activity ID. Your state accessor could look like this:
public IStatePropertyAccessor<Dictionary<string, string>> CardStateAccessor { get; internal set; }
And you can initialize it like this:
CardStateAccessor = _conversationState.CreateProperty<Dictionary<string, string>>("cardState");
Since your card is in JSON form, you may want to deserialize it before adding a unique card ID to the submit action:
var card = JObject.Parse(json);
var data = card.SelectToken("actions[0].data");
var cardId = Guid.NewGuid();
data[KEYCARDID] = cardId;
var cardActivity = MessageFactory.Attachment(new Attachment("application/vnd.microsoft.card.adaptive", content: card));
var response = await turnContext.SendActivityAsync(cardActivity, cancellationToken);
var dict = await CardStateAccessor.GetAsync(turnContext, () => new Dictionary<string, string>(), cancellationToken);
dict[cardId] = response.Id;
Then you can delete the activity like this:
var value = JObject.FromObject(turnContext.Activity.Value);
var cardId = Convert.ToString(value[KEYCARDID]);
var dict = await CardStateAccessor.GetAsync(turnContext, () => new Dictionary<string, string>(), cancellationToken);
if (dict.TryGetValue(cardId, out var activityId))
{
await turnContext.DeleteActivityAsync(activityId, cancellationToken);
dict.Remove(cardId);
}
If you would like this process to be made easier then you may voice your support for my cards library proposal: https://github.com/BotBuilderCommunity/botbuilder-community-dotnet/issues/137
I would like to understand how to read/write data with Adaptive cards. I can read the data from a submit action, and reply as text, but not sure how present the input data in the card. First place, I would like to add the shootValue to an array that I can carry trough the lifecycle of the card. Can somebody please let me know how to do this?
The goal of this question is to understand how to keep existing responses from the card.
Like in Battleship, I shoot "A1", type it in an input box, submit, I would like to see "A1" in the card. I add "A2", submit, then I would like to see "A1" and "A2" in the card that is sent to Teams. I understand that I need to rebuild the card from scratch at every shot, that means, I need to either carry on the shots somehow with each action.
Data card:
{
"type": "AdaptiveCard",
"version": "1.0",
"body": [
{
"type": "TextBlock",
"text": "Hello {name}"
},
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"width": "stretch",
"id": "",
"items": [
{
"type": "Container",
"items": [
{
"type": "Input.Text",
"placeholder": "Voorbeeld: A1",
"id": "id_shoot",
"$data": "shoot"
}
]
}
]
},
{
"type": "Column",
"width": "stretch",
"items": [
{
"type": "Container",
"items": [
{
"type": "TextBlock",
"text": " {shoot}",
"horizontalAlignment": "Right",
"id": ""
}
],
"$data": "{shoots}",
"id": "shotcoords"
}
],
"$data": "{shots}"
},
{
"type": "Column",
"width": "stretch",
"items": [
{
"type": "Container",
"items": [
{
"type": "TextBlock",
"text": "{status}",
"id": ""
}
],
"$data": "{shoots}",
"id": "shotstatuses"
}
],
"id": ""
}
]
},
{
"type": "ActionSet",
"id": "",
"actions": [
{
"type": "Action.Submit",
"title": "Shoot",
"id": "",
"style": "positive",
"data": {}
}
]
}
],
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json"
}
Data
{
"name": "Test shot",
"shoots": [
{
"shoot": "a1",
"status": "hit"
},
{
"shoot": "a2",
"status": "hit"
}
]
}
There is no "simple" way to do this, but there is a way. The answer will be similar to this one.
First, you'll need a way of saving state for your card so you can update the card's activity. In C# you can declare a state property accessor like this:
public IStatePropertyAccessor<Dictionary<string, (string ActivityId, List<string> Shots)>> BattleshipStateAccessor { get; internal set; }
Then you can instantiate it like this
BattleshipStateAccessor = _conversationState.CreateProperty<Dictionary<string, (string, List<string>)>>("battleshipState");
You have a few decisions to make here. First, I'm choosing to make the state property a dictionary so I can keep track of multiple cards and only update the specific card that was clicked. If you don't care about that then you don't need a dictionary and you don't need to worry about "card ID's," but saving at least one activity ID is necessary so that you'll be able to update the card. As for saving the "shots," you have a few choices here. You could save that state on the client side by updating the submit action's data with each shot that's been made, but I figure I might as well save the shots in bot state because I already need to save the activity ID in bot state anyway. Then there's the question of what information about each shot you should save. In this example I'm only saving the location of the shot that the user entered and not the status of the shot, since I figure I can always recalculate the status whenever I need to.
I've modified your submit action to look like this:
{
"type": "Action.Submit",
"title": "Shoot",
"style": "positive",
"data": {
"behavior": "Shoot",
"cardId": ""
}
}
What I've done here is added two properties to your data object, and this data will be sent to your bot along with the text input's value. The "behavior" property will help your bot route to the correct function in case your bot uses multiple types of actions that can be handled different ways. The "cardId" property is just a placeholder that your bot code will fill in when creating the card. I've stored the names of these properties in the constants KEYBEHAVIOR and KEYCARDID.
You'll want a consistent way to generate your card that you can use when you send the card initially and when you update the card.
internal static IMessageActivity CreateBattleshipCardActivity(
string cardId,
object data = null)
{
data = data ?? new
{
name = "Test shot",
shoots = new string[0],
};
JObject card = CreateAdaptiveCard("battleship", data);
foreach (var token in card.Descendants()
.Select(token => token as JProperty)
.Where(token => token?.Name == KEYCARDID))
{
token.Value = cardId;
}
return MessageFactory.Attachment(new Attachment(
AdaptiveCard.ContentType,
content: card));
}
The CreateAdaptiveCard function loads the JSON template from a file with the given name, transforms it with the given data, and deserializes it into a JObject.
Using this function, you can send the card initially like this in C#:
public async Task TestBattleshipAsync(
ITurnContext turnContext,
CancellationToken cancellationToken)
{
var activity = turnContext.Activity;
var cardId = Guid.NewGuid().ToString();
var reply = CreateBattleshipCardActivity(cardId);
var response = await turnContext.SendActivityAsync(reply, cancellationToken);
var dict = await BattleshipStateAccessor.GetAsync(
turnContext,
() => new Dictionary<string, (string, List<string>)>(),
cancellationToken);
dict[cardId] = (response.Id, new List<string>());
}
And you can update the card in response to the card's "Shoot" submit action like this:
private async Task ShootAsync(
ITurnContext turnContext,
CancellationToken cancellationToken)
{
var activity = turnContext.Activity;
if (activity.ChannelId == Channels.Msteams)
{
var value = JObject.FromObject(activity.Value);
var cardId = Convert.ToString(value[BotUtil.KEYCARDID]);
var dict = await BattleshipStateAccessor.GetAsync(
turnContext,
() => new Dictionary<string, (string, List<string>)>(),
cancellationToken);
if (dict.TryGetValue(cardId, out var savedInfo))
{
savedInfo.Shots.Add(value["id_shoot"].ToString());
var data = new
{
name = "Test shot",
shoots = savedInfo.Shots.Select(shot => new
{
shoot = shot,
status = DetermineHit(shot),
}),
};
var update = CreateBattleshipCardActivity(cardId, data);
update.Id = savedInfo.ActivityId;
update.Conversation = activity.Conversation;
await turnContext.UpdateActivityAsync(update, cancellationToken);
}
}
}
I have chat Bot developed for Web channel using MS Bot Framework SDK V4 in C# which has multiple waterfall dialog classes each performs specific task. In main root dialog i have set of options displayed Option 1,2,3,4...6. Now when i select an option 5 i get redirected to a new dialog class where
I have an adaptive card that i designed with 3 sets of containers one takes input text through text boxes and second container have some check boxes to be selected and third container contains 2 buttons submit and cancel button. For these buttons i have put data as Cancel = 0 and 1 respectively.
In this option 5 dialog i am controlling based on the data cancel-0 or 1 if it is 1 i am doing end dialog and displaying default display options 1,2,3,4...6.
Now, i clicked on submit button by entering valid values and the process has been completed successfully as a result the current dialog has ended and again the main set of options are displayed.
Here i did some kind of negative testing where i scrolled up and clicked cancel button which was displayed above. This resulted the first option(option 1 ) displayed in the set of options 1 to 6 selected y default and that option operations got performed automatically even though i selected cancel and not the first option. But this is not happening when i select submit button displayed in adaptive card after scrolling up it is displaying the retry prompt to select any one of the following options where as when i clicked on cancel it is going to 1st option by default.
Please find the dialog related and adaptive card related data below:
{
"type": "AdaptiveCard",
"body": [
{
"type": "TextBlock",
"size": "Large",
"weight": "Bolder",
"text": "Request For Model/License",
"horizontalAlignment": "Center",
"color": "Accent",
"id": "RequestforModel/License",
"spacing": "None",
"wrap": true
},
{
"type": "Container",
"items": [
{
"type": "TextBlock",
"text": "Requester Name* : ",
"id": "RequesterNameLabel",
"weight": "Bolder",
"wrap": true,
"spacing": "None"
},
{
"type": "Input.Text",
"placeholder": "Enter Requester Name",
"id": "RequesterName",
"spacing": "None"
},
{
"type": "TextBlock",
"text": "Requester Email* : ",
"id": "RequesterEmailLabel",
"weight": "Bolder",
"wrap": true,
"spacing": "Small"
},
{
"type": "Input.Text",
"placeholder": "Enter Requester Email",
"id": "RequesterEmail",
"style": "Email",
"spacing": "None"
},
{
"type": "TextBlock",
"text": "Customer Name* : ",
"id": "CustomerNameLabel",
"weight": "Bolder",
"wrap": true,
"spacing": "Small"
},
{
"type": "Input.Text",
"placeholder": "Enter Customer Name",
"id": "CustomerName",
"spacing": "None"
},
{
"type": "TextBlock",
"text": "Select Request Type : ",
"id": "RequestTypeText",
"horizontalAlignment": "Left",
"wrap": true,
"weight": "Bolder",
"size": "Medium",
"spacing": "Small"
},
{
"type": "Input.ChoiceSet",
"placeholder": "--Select--",
"choices": [
{
"title": "Both",
"value": "Both"
},
{
"title": "1",
"value": "1"
},
{
"title": "2",
"value": "2"
}
],
"id": "RequestType",
"value": "Both",
"spacing": "None"
}
],
"horizontalAlignment": "Left",
"style": "default",
"bleed": true,
"id": "Requesterdata"
},
{
"type": "Container",
"items": [
{
"type": "TextBlock",
"text": "Select Asset* :",
"id": "Assetheader",
"horizontalAlignment": "Left",
"wrap": true,
"weight": "Bolder",
"size": "Medium",
"spacing": "Small"
},
{
"type": "Input.ChoiceSet",
"placeholder": "",
"choices": [
{
"title": "chekcbox1",
"value": "chekcbox1"
},
{
"title": "chekcbox2",
"value": "chekcbox2"
},
{
"title": "chekcbox3",
"value": "chekcbox3"
},
{
"title": "chekcbox4",
"value": "chekcbox4"
},
{
"title": "chekcbox5",
"value": "chekcbox5"
}
],
"isMultiSelect": true,
"id": "AssetsList",
"wrap": true,
"spacing": "None"
}
],
"id": "Assetdata",
"style": "default",
"horizontalAlignment": "Left",
"bleed": true
},
{
"type": "Container",
"items": [
{
"type": "ActionSet",
"actions": [
{
"type": "Action.Submit",
"title": "Cancel",
"id": "CanclBtn",
"style": "positive",
"data": {
"Cancel": 1
}
},
{
"type": "Action.Submit",
"title": "Submit",
"id": "SubmitBtn",
"style": "positive",
"data": {
"Cancel": 0
}
}
],
"id": "Action1",
"horizontalAlignment": "Center",
"spacing": "Small",
"separator": true
}
]
}
],
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.0",
"id": "ModelLicenseRequestForm",
"lang": "Eng"
}
Code below of main root dialog:
AddStep(async (stepContext, cancellationToken) =>
{
return await stepContext.PromptAsync(
"choicePrompt",
new PromptOptions
{
Prompt = stepContext.Context.Activity.CreateReply("Based on the access privileges assigned to you by your admin, below are the options you can avail. Please click/choose any one from the following: "),
Choices = new[] { new Choice { Value = "option1" }, new Choice { Value = "option2" }, new Choice { Value = "option3" }, new Choice { Value = "option4" }, new Choice { Value = "option5" }, new Choice { Value = "option6" } }.ToList(),
RetryPrompt = stepContext.Context.Activity.CreateReply("Sorry, I did not understand that. Please choose any one from the options displayed below: "),
});
});
AddStep(async (stepContext, cancellationToken) =>
{
if (response == "option1")
{
doing something
}
if (response == "option2")
{
return await stepContext.BeginDialogAsync(option2.Id, cancellationToken: cancellationToken);
}
if (response == "option3")
{
return await stepContext.BeginDialogAsync(option3.Id, cancellationToken: cancellationToken);
}
if (response == "option4")
{
return await stepContext.BeginDialogAsync(option4.Id, cancellationToken: cancellationToken);
}
if (response == "option5")
{
return await stepContext.BeginDialogAsync(option5.Id, cancellationToken: cancellationToken);
}
if (response == "option6")
{
return await stepContext.BeginDialogAsync(option6.Id, cancellationToken: cancellationToken);
}
return await stepContext.NextAsync();
});
option 5 dialog class code:
AddStep(async (stepContext, cancellationToken) =>
{
var cardAttachment = CreateAdaptiveCardAttachment("Adaptivecard.json");
var reply = stepContext.Context.Activity.CreateReply();
reply.Attachments = new List<Microsoft.Bot.Schema.Attachment>() { cardAttachment };
await stepContext.Context.SendActivityAsync(reply, cancellationToken);
var opts = new PromptOptions
{
Prompt = new Activity
{
Type = ActivityTypes.Message,
// You can comment this out if you don't want to display any text. Still works.
}
};
// Display a Text Prompt and wait for input
return await stepContext.PromptAsync(nameof(TextPrompt), opts);
});
AddStep(async (stepContext, cancellationToken) =>
{
var res = stepContext.Result.ToString();
dynamic modelrequestdata = JsonConvert.DeserializeObject(res);
string canceloptionvalidaiton = modelrequestdata.Cancel;
if (canceloptionvalidaiton == "0")
{
// ...perform operation
return await stepContext.EndDialogAsync();
}
else
{
return await stepContext.EndDialogAsync();
}
});
Please note i have purposefully did not provide the whole code for easy understanding and other purposes.
The main idea for me to keep the cancel button is to cancel the current operation so that user can go to main dialog options select any other task to perform
The query is:
How to enable cancel button in adaptive card if my above logic is not correct?
can we have cancel button in adaptive card? or is it a wrong assumption and we cannot have cancel option?
Updated on Nov 8,2019
The below update is for clear and better understanding of my query:
1) When the BOT is launched through Web Channel main root dialog is fired in back end which has all the dialog's and things added to the stack:
Below is the main root dialog class code:
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.Dialogs.Choices;
using Microsoft.Bot.Schema;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
namespace EchoBot.Dialogs
{
public class MainRootDialog : ComponentDialog
{
public MainRootDialog(UserState userState)
: base("root")
{
_userStateAccessor = userState.CreateProperty<JObject>("result");
AddDialog(DisplayOptionsDialog.Instance);
AddDialog(Option1.Instance);
AddDialog(Option2.Instance);
AddDialog(Option3.Instance);
AddDialog(Option4.Instance);
AddDialog(Option5.Instance);
AddDialog(Option6.Instance);
AddDialog(new ChoicePrompt("choicePrompt"));
InitialDialogId = DisplayOptionsDialog.Id;
}
}
}
2) Since the initial dialog is displayedoptionsdialog as a result the following prompt options are displayed in front end to user:
Option1 Option2 Option3 Option4 Option5 Option6
This i achieved through following code which i have written in a class named DisplayOptionsDialog:
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.Dialogs.Choices;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace EchoBot.Dialogs
{
public class DisplayOptionsDialog : WaterfallDialog
{
public DisplayOptionsDialog(string dialogId, IEnumerable<WaterfallStep> steps = null)
: base(dialogId, steps)
{
AddStep(async (stepContext, cancellationToken) =>
{
return await stepContext.PromptAsync(
"choicePrompt",
new PromptOptions
{
Prompt = stepContext.Context.Activity.CreateReply("Below are the options you can avail. Please click/choose any one from the following: "),
Choices = new[] { new Choice { Value = "Option1" }, new Choice { Value = "Option2" }, new Choice { Value = "Option3" }, new Choice { Value = "Option4" }, new Choice { Value = "Option5" }, new Choice { Value = "Option6" }}.ToList(),
RetryPrompt = stepContext.Context.Activity.CreateReply("Sorry, I did not understand that. Please choose any one from the options displayed below: "),
});
});
AddStep(async (stepContext, cancellationToken) =>
{
var response = (stepContext.Result as FoundChoice)?.Value;
if (response == "Option1")
{
await stepContext.Context.SendActivityAsync(MessageFactory.Text($"Otpion 1 selected")); //Here there is lot of actual data printing that i am doing but due //to some sensitive inoformation i have kept a simple statment that gets //displayed but in actual code it is just printing back or responding back few //statements which again printing only
}
if (response == "Option2")
{
return await stepContext.BeginDialogAsync(Option2.Id, cancellationToken: cancellationToken);
}
if (response == "Option3")
{
return await stepContext.BeginDialogAsync(Option3.Id, cancellationToken: cancellationToken);
}
if (response == "Option4")
{
return await stepContext.BeginDialogAsync(Option4.Id, cancellationToken: cancellationToken);
}
if (response == "Option5")
{
return await stepContext.BeginDialogAsync(Option5.Id, cancellationToken: cancellationToken);
}
if (response == "Option6")
{
return await stepContext.BeginDialogAsync(Option6.Id, cancellationToken: cancellationToken);
}
return await stepContext.NextAsync();
});
AddStep(async (stepContext, cancellationToken) =>
{
return await stepContext.ReplaceDialogAsync(Id);
});
}
public static new string Id => "DisplayOptionsDialog";
public static DisplayOptionsDialog Instance { get; } = new DisplayOptionsDialog(Id);
}
}
3) Since the issue w.r.t user selecting Option5 i'll directly go to the option5 dialog class code:
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Schema;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace EchoBot.Dialogs
{
public class Option5Dialog : WaterfallDialog
{
public const string cards = #"./ModelAdaptivecard.json";
public Option5Dialog(string dialogId, IEnumerable<WaterfallStep> steps = null)
: base(dialogId, steps)
{
AddStep(async (stepContext, cancellationToken) =>
{
var cardAttachment = CreateAdaptiveCardAttachment(cards);
var reply = stepContext.Context.Activity.CreateReply();
reply.Attachments = new List<Microsoft.Bot.Schema.Attachment>() { cardAttachment };
await stepContext.Context.SendActivityAsync(reply, cancellationToken);
var opts = new PromptOptions
{
Prompt = new Activity
{
Type = ActivityTypes.Message,
// You can comment this out if you don't want to display any text. Still works.
}
};
// Display a Text Prompt and wait for input
return await stepContext.PromptAsync(nameof(TextPrompt), opts);
});
AddStep(async (stepContext, cancellationToken) =>
{
var activityTextformat = stepContext.Context.Activity.TextFormat;
if (activityTextformat == "plain")
{
await stepContext.Context.SendActivityAsync(MessageFactory.Text($"Sorry, i did not understand that please enter proper details in below displayed form and click on submit button for processing your request"));
return await stepContext.ReplaceDialogAsync(Id, cancellationToken: cancellationToken);
}
else
{
var res = stepContext.Result.ToString();
dynamic modelrequestdata = JsonConvert.DeserializeObject(res);
string canceloptionvalidaiton = modelrequestdata.Cancel;
if (canceloptionvalidaiton == "0")
{
string ServiceRequesterName = modelrequestdata.RequesterName;
string ServiceRequesterEmail = modelrequestdata.RequesterEmail;
string ServiceRequestCustomerName = modelrequestdata.CustomerName;
string ServiceRequestType = modelrequestdata.RequestType;
string ServiceRequestAssetNames = modelrequestdata.AssetsList;
//checking wehther data is provided or not
if (string.IsNullOrWhiteSpace(ServiceRequesterName) || string.IsNullOrWhiteSpace(ServiceRequesterEmail) || string.IsNullOrWhiteSpace(ServiceRequestCustomerName) || string.IsNullOrWhiteSpace(ServiceRequestAssetNames))
{
await stepContext.Context.SendActivityAsync(MessageFactory.Text($"Mandatory fields such as Requester name,Requester Email,Cusomter Name or Asset details are not selected are not provided"));
return await stepContext.ReplaceDialogAsync(Id, cancellationToken: cancellationToken);
}
else
{
await stepContext.Context.SendActivityAsync(MessageFactory.Text("Data recorded successfully"));
await stepContext.Context.SendActivityAsync(MessageFactory.Text("Thank You!.Looking forward to see you again."));
return await stepContext.EndDialogAsync();
}
}
else
{
await stepContext.Context.SendActivityAsync(MessageFactory.Text("Looks like you have cancelled the Model/License request"));
await stepContext.Context.SendActivityAsync(MessageFactory.Text("Thank You!.Looking forward to see you again."));
return await stepContext.EndDialogAsync();
}
}
});
}
public static new string Id => "Option5Dialog";
public static Option5Dialog Instance { get; } = new Option5Dialog(Id);
public static Microsoft.Bot.Schema.Attachment CreateAdaptiveCardAttachment(string filePath)
{
var adaptiveCardJson = File.ReadAllText(filePath);
var adaptiveCardAttachment = new Microsoft.Bot.Schema.Attachment()
{
ContentType = "application/vnd.microsoft.card.adaptive",
Content = JsonConvert.DeserializeObject(adaptiveCardJson),
};
return adaptiveCardAttachment;
}
}
}
Here are things happened or observed during this process of option5 and other things in both positive testing or negative testing:
User provided data in adaptive card displayed as part of Option5 and clicked on submit button user gets the message request id created and etc as shown in above code at same time the dialog is ended and the same default options of Option1 to 6 are displayed as part of defaultDisplayoptions dialog class
Now user scrolls up again and clicks on submit button but as we observe user is in defaultoptions dialog as per code and also as per displayed options
the user is displayed:
Sorry, I did not understand that. Please choose any one from the options displayed below:
Option1 Option2 Option3 Option4 Option5 Option6
This is working as needed and as expected so no problems here.
This is the same case how many times ever i click on Submit button
Now i went up and clicked on cancel button this time the control directly went to Displayoptions->Option1 and the statement present in that block got printed
When i debugged i notice the stepcontext in the displayoptions dialog has the text value or choice pre-filled or pre-selected as Option1 without me selecting that option as a result it is printing the statements under it.
Not sure how it is doing it and why it is doing it. So i thought my self may be to include cancel button this way(the way i have done) is wrong may be there is another way and i asked the query how to achieve the cancel button functionality in adaptive card in this post.
However, if what i have done is correct way can you please tell me why the issue is w.r.t only cancel button where when control goes to the DiaplayOptions dialog the option 1 gets pre-selected somehow where as everything works fine w.r.t Submit button(no issues at all in this case any time).
Can you please help me regarding the issue considering my updated information and query?
I have received your code over email and managed to extract the answers to some of my questions.
We know that you must be manipulating the turn context's activity before passing it to the dialog, or else your text prompts could not work with object-based submit actions.
The code I was asking for is in your DialogExtensions.Run class:
Activity activity = dialogContext.Context.Activity;
object rawChannelData = activity.ChannelData;
if (dialogContext.Context.Activity.Value != null && dialogContext.Context.Activity.Text == null)
{
dialogContext.Context.Activity.Text = turnContext.Activity.Value.ToString();
}
You can see that this is a bad place to put it, since you apparently forgot it was even there. Another reason that's a bad place to put it is that you should be using the builtin DialogExtensions.RunAsync method instead.
What's happening is that you're passing the serialized JSON from the Adaptive Card's submit action into whatever dialog is active. So if the active dialog is a choice prompt, it will try to interpret that serialized JSON as one of the choices. When the cancel button is clicked, that JSON will contain "Cancel": 1, and the 1 makes the recognizer think you want to go with option 1.
The easiest solution is of course to just rework your Adaptive Card so that it won't contain any numbers, but of course that would be an ad hoc fix that may not work for all your future scenarios.
You haven't actually said what your expected/desired behavior is, but I can think of two main options:
You want the submit actions to be ignored when they're clicked outside of that one prompt
You want the cancel button to cancel any dialog no matter which dialog is active
I can guess by your code that you probably intended for the buttons to only work in that one prompt. Since you're using Web Chat, you might consider a client-side solution where you'd make your own Adaptive Cards renderer that allows submit actions to be disabled after the card is used. I presume that solution is more difficult than you'd like, but there are also ways to have the bot ignore submit actions under specific circumstances. You can have a look at Michael Richardson's Adaptive Card prompt for some ideas, and also vote up my Adaptive Cards community project.
If you want the cancel button to work for any dialog, just make sure you respond to its activity by calling CancelAllDialogsAsync instead of ContinueDialogAsync.
How is "response" generated in your main root dialog
This is the line I was asking for:
var response = (stepContext.Result as FoundChoice)?.Value;
You had inexplicably omitted that line from your "main root dialog," though I notice the line was included when you redundantly pasted that code under the name DisplayOptionsDialog. In the future you will be able to receive better help faster if you don't leave out vital information, or at the very least provide it when asked.
Please refer to my latest blog post for more information about using Adaptive Cards with the Bot Framework.
I am currently trying to create an adaptive card in a Waterfall Dialog for one of my bots that will display the name and search item (both strings) when rendered. Both of the values I want to use are stored in the Context.Activity.Value property of my dialog, so all I need to know is how to insert those values into my adaptive card at some point during its creation so that the "text" values of the text blocks can contain my values.
I have looked into using empty JSON objects in the adaptive card schema that I could fill somehow during the adaptive card's creation, but have not figured out how to insert said values. I'm a relative beginner with C# and Bot Framework, so I don't know what to try.
Below is the step in my Waterfall Dialog where the adaptive card is made:
private async Task<DialogTurnResult> AdaptiveCardTest(WaterfallStepContext stepContext,
CancellationToken cancellationToken)
{
var introCard = File.ReadAllText("./Content/AdaptiveCardTest.json");
var card = AdaptiveCard.FromJson(introCard).Card;
var attachment = new Attachment(AdaptiveCard.ContentType, content: card);
var response = MessageFactory.Attachment(attachment, ssml: card.Speak,
inputHint: InputHints.AcceptingInput);
await stepContext.Context.SendActivityAsync(response);
return await stepContext.NextAsync();
}
AdaptiveCardTest.json is the adaptive card's json file. At the moment it just has an image popup with some text which includes placeholders where I would like the user name and search item to go. The placeholder links are there because the actual links are ridiculously long.
{
"type": "AdaptiveCard",
"id": "NewUserGreeting",
"backgroundImage": "image_url_placeholder"
"body": [
{
"type": "Container",
"items": [
{
"type": "Image",
"url": "image_url_placeholder_2"",
"size": "Stretch"
}
]
},
{
"type": "Container",
"spacing": "None",
"backgroundImage": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAXCAIAAACAiijJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAqSURBVDhPY1RgL2SgDDBBaQrAqBEIMGoEAowagQCjRiDAqBEIQLERDAwAIisAxhgAwtEAAAAASUVORK5CYII=",
"items": [
{
"type": "TextBlock",
"id": "title",
"spacing": "Medium",
"size": "Large",
"weight": "Bolder",
"color": "Light",
"text": "Hi, I'm **your** Virtual Assistant",
"wrap": true
},
{
"type": "TextBlock",
"id": "body",
"size": "Medium",
"color": "Light",
"text": "The user {{Name}} would like to know more about {{SearchItem}}.",
"wrap": true
}
]
}
],
}
Any help would be greatly appreciated, thank you!
For your simple scenario I would go with #MikeP's suggestion. In the future if you want to do something more complex where a template won't suffice, then you can build the Adaptive Card dynamically using the .NET SDK once you have installed the AdaptiveCard NuGet package.
The documentation on the .NET SDK is pretty limited but the properties of the AdaptiveCard object usually align with their JSON counterparts.
An example is:
const string ISO8601Format = "yyyy-MM-dd";
string text = "dynamic-text-here;
DateTime today = DateTime.Today;
string todayAsIso = today.ToString(ISO8601Format);
// Create card
AdaptiveCard adaptiveCard = new AdaptiveCard("1.0")
{
Body =
{
new AdaptiveContainer
{
Items =
{
new AdaptiveTextBlock
{
Text = question,
Wrap = true
},
new AdaptiveDateInput
{
// This Id matches the property in DialogValueDto so it will automatically be set
Id = "UserInput",
Value = todayAsIso,
Min = today.AddDays(-7).ToString(ISO8601Format),
Max = todayAsIso,
Placeholder = todayAsIso
}
}
}
},
Actions = new List<AdaptiveAction>
{
new AdaptiveSubmitAction
{
// Data can be an object but this will require the value provided for the
// Content property to be serialised it to a string
// as per this answer https://stackoverflow.com/a/56297792/5209435
// See the attachment block below for how this is handled
Data = "your-submit-data",
Title = "Confirm",
Type = "Action.Submit"
}
}
};
// Create message attachment
Attachment attachment = new Attachment
{
ContentType = AdaptiveCard.ContentType,
// Trick to get Adapative Cards to work with prompts as per https://github.com/Microsoft/botbuilder-dotnet/issues/614#issuecomment-443549810
Content = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(adaptiveCard))
};
cardActivity.Attachments.Add(attachment);
// Send the message
context.SendActivityAsync(cardActivity);
Because Items and Actions are collections, you could have conditional logic inside your code to build up these collections based on some condition at runtime then pass the build collection to Items or Actions which will allow you more flexibility than having a JSON template that you replace placeholder tokens at a known location.
This is something that I have done in the past using Handlebars which is a nice way of replacing tokens in your adaptive card JSON with properties from a model. Just make sure the tokens in your adaptive card JSON match the model properties
Have a look at their site for more detail but it is just a case of doing:
Handlebars.Compile(<Adaptive card template>);
Handlebars is available as a Nuget package you can add into your project.
Hello i have this input forms card. It is rendering properly but how can i get its results? And how can i make it so that the bot wait for the user to submit before proceding to the next step? Putting stepContext.NextAsync will automatically trigger the next step. But removing it will cause an error because it needs to return something.
public InitialQuestions(string dialogId, IEnumerable<WaterfallStep> steps = null)
: base(dialogId, steps)
{
AddStep(async (stepContext, cancellationToken) =>
{
var cardAttachment = CreateAdaptiveCardAttachment(_cards);
var reply = stepContext.Context.Activity.CreateReply();
reply.Attachments = new List<Attachment>() { cardAttachment };
await stepContext.Context.SendActivityAsync(reply, cancellationToken);
// how can i wait for user to click submit before going to next step?
return await stepContext.NextAsync();
// return await stepContext.PromptAsync(
// "textPrompt",
// new PromptOptions
// {
// Prompt = MessageFactory.Text(""),
// },
// cancellationToken: cancellationToken);
});
AddStep(async (stepContext, cancellationToken) =>
{
// next step
});
}
private static Attachment CreateAdaptiveCardAttachment(string filePath)
{
var adaptiveCardJson = File.ReadAllText(filePath);
var adaptiveCardAttachment = new Attachment()
{
ContentType = "application/vnd.microsoft.card.adaptive",
Content = JsonConvert.DeserializeObject(adaptiveCardJson),
};
return adaptiveCardAttachment;
}
This is the card
{
"type": "AdaptiveCard",
"body": [
{
"type": "TextBlock",
"text": "What is your Occupation?"
},
{
"type": "Input.Text",
"id": "Occupation",
"placeholder": "Occupation"
},
{
"type": "TextBlock",
"text": "Are you married? "
},
{
"type": "Input.ChoiceSet",
"id": "Married",
"value": "true",
"choices": [
{
"title": "Yes",
"value": "true"
},
{
"title": "No",
"value": "false"
}
],
"style": "expanded"
},
{
"type": "TextBlock",
"text": "When is your birthday?"
},
{
"type": "Input.Date",
"id": "Birthday",
"value": ""
}
],
"actions": [
{
"type": "Action.Submit",
"title": "Submit",
"data": {
"id": "1234567890"
}
}
],
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.0"
}
Thanks guys.
EDIT: for future reference of others this is the answer i found.
AddStep(async (stepContext, cancellationToken) =>
{
var state = await (stepContext.Context.TurnState["BasicAccessors"] as BasicAccessors).BasicStateAccessor.GetAsync(stepContext.Context);
var jsonString = (JObject)stepContext.Context.Activity.Value;
BasicState results = JsonConvert.DeserializeObject<BasicState>(jsonString.ToString());
state.Occupation = results.Occupation;
state.Married = results.Married;
state.Birthday = results.Birthday;
return await stepContext.NextAsync();
});
Let me answer your questions in reverse order:
And how can i make it so that the bot wait for the user to submit before proceding to the next step? Putting stepContext.NextAsync will automatically trigger the next step. But removing it will cause an error because it needs to return something.
Yes, it's true, you need to return something from your step, but as you point out you're not ready for it to move to the next step yet. The answer is that you want to use a prompt at this point! Now I see you have some code in here commented out to do this and maybe what's confusing is that, today, there is no specific prompt for working with cards. Instead you do want to use a general purpose TextPrompt and we'll set the activity on that to something other than just simple text.
With this in mind, you would keep your code above that is using CreateReply to build your Activity with card attachments, but, instead of sending that Activity yourself with SendActivityAsync you want to set it as the value of the Prompt property of the TextPrompt like so:
AddStep(async (stepContext, cancellationToken) =>
{
return await stepContext.PromptAsync(
"myPrompt",
new PromptOptions
{
Prompt = new Activity
{
Type = ActivityTypes.Message,
Attachments = new List<Attachment>()
{
CreateAdaptiveCardAttachment(_cards),
},
},
},
cancellationToken: cancellationToken);
});
Ok, so that's one half of the problem. With that in mind now, let's circle back to the first part of your question:
Hello i have this input forms card. It is rendering properly but how can i get its results?
Well, your adaptive card is using the Submit action which means that you will receive an activity that contains the values of the form in the Values property of the Activity, however because we used a TextPrompt above the default validation behavior of the TextPrompt is going to be validating that some value was supplied for the Text portion of the Activity which there won't be in this case. So, to fix that, when you configure the TextPrompt you really want to provide your own PromptValidator<T> like so:
Add(new TextPrompt("myPrompt", new PromptValidator<string>(async (pvc, ct) => true)));
This basically says the input is valid no matter what. You could make it richer if you wanted by actually checking the details of the value, but this should unblock you for now.
Now, back in your WaterfallDialog your next step is going to be receiving the Activity whose Value property will be a JObject which you can either use directly or you can call JObject::ToObject<T> to convert it into a specific class you've created that represents your form input:
AddStep(async (stepContext, cancellationToken) =>
{
// This will give you a JObject representation of the incoming values
var rawValues = (JObject)stepContext.Context.Activity.Values;
// You can convert that to something more strongly typed like so
// where MyFormValues is a class you've defined
var myFormValues = rawValues.ToObject<MyFormValues>();
});
I want to just close this answer out by saying that, in answering your question, I've recorded a bunch of feedback that I intend to send to the product team to improve this situation both in terms of API design and documentation because, clearly, this is not obvious or optimal.