如何在 C# 的 MS Bot 框架 SDK V4 中开发的 Web Channel 聊天机器人中显示自适应卡片中的取消按钮?

How to have cancel button in Adaptive cards to be displayed in Web Channel chat bot developed in MS Bot framework SDK V4 in C#?

我使用 C# 中的 MS Bot Framework SDK V4 为 Web 频道开发了聊天机器人,它有多个瀑布对话框 classes,每个都执行特定的任务。在主根对话框中,我有一组选项显示选项 1、2、3、4...6。现在,当我 select 一个选项 5 时,我被重定向到一个新对话框 class,其中

我有一个自适应卡,我设计了 3 组容器,一个通过文本框接收输入文本,第二个容器有一些要 selected 的复选框,第三个容器包含 2 个按钮提交和取消按钮.对于这些按钮,我分别将数据设置为 Cancel = 0 和 1。 在此选项 5 对话框中,我根据数据 cancel-0 或 1(如果它是 1)进行控制,我正在结束对话框并显示默认显示选项 1、2、3、4...6。 现在,我通过输入有效值单击提交按钮,并且由于当前对话框结束并再次显示主要选项集,该过程已成功完成。

我在这里做了一些负面测试,我向上滚动并点击上面显示的取消按钮。这导致第一个选项(选项 1)显示在选项 1 到 6 的集合中 selected y default 并且即使我 selected 取消而不是第一个选项,该选项操作也会自动执行。但是当我 select 向上滚动后自适应卡中显示的提交按钮显示重试提示 select 以下任何一个选项时,这并没有发生,当我点击取消时它会默认第一个选项。

请在下面找到对话框相关和自适应卡片相关数据:

{
    "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"
}

主根对话框下面的代码:

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();
});

选项 5 对话框 class 代码:

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();
    }
});

请注意,为了便于理解和其他目的,我故意没有提供整个代码。

我保留取消按钮的主要想法是取消当前操作,以便用户可以转到主对话框选项select要执行的任何其他任务

查询是:

  1. 如果我的上述逻辑不正确,如何在自适应卡中启用取消按钮?
  2. 适配卡可以有取消按钮吗?或者这是一个错误的假设,我们不能取消选项?

2019 年 11 月 8 日更新

以下更新是为了更清楚和更好地理解我的查询:

1) 当 BOT 通过 Web Channel 启动时,在后端触发主根对话框,所有对话框和内容都添加到堆栈中:

下面是主根对话框 class 代码:

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) 由于初始对话框显示选项对话框,因此在前端向用户显示以下提示选项:

选项1 选项2 选项3 选项4 选项5 选项6

这是我通过以下代码实现的,这些代码是我在名为 DisplayOptionsDialog 的 class 中编写的:

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) 由于问题 w.r.t 用户 selecting Option5 我将直接转到选项 5 对话框 class 代码:

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;
        }
    }
}

以下是在 option5 的这个过程中发生或观察到的事情,以及在正面测试或负面测试中的其他事情:

  1. 用户在自适应卡中提供的数据作为 Option5 的一部分显示并单击提交按钮用户获取创建的消息请求 ID 等,如上面的代码所示,同时对话框结束并且相同Option1 到 6 的默认选项显示为 defaultDisplayoptions 对话框的一部分 class

  2. 现在用户再次向上滚动并点击提交按钮,但我们观察到用户根据代码和显示的选项处于默认选项对话框中

用户显示: 对不起,我不明白。请从下面显示的选项中选择一个:

选项1 选项2 选项3 选项4 选项5 选项6

这是按需和预期工作的,所以这里没有问题。

  1. 我点击提交按钮多少次都是这样

  2. 现在我上去点击取消按钮,这次控件直接转到 Displayoptions->Option1 并且打印了该块中的语句

当我调试时,我注意到显示选项对话框中的 stepcontext 有文本值或选项预填充或预select编辑为 Option1 而没有我 selecting 该选项结果它正在打印其下的语句。

不确定它是如何做的以及为什么要这样做。所以我认为我自己可能会以这种方式包含取消按钮(我所做的方式)是错误的可能还有另一种方式,我询问了如何在自适应卡中实现取消按钮功能的问题 post .

但是,如果我所做的是正确的方法,你能告诉我为什么问题是 w.r.t 只有取消按钮,当控制转到 DiaplayOptions 对话框时,选项 1 得到预 select 以某种方式编辑,因为一切正常 w.r.t 提交按钮(在这种情况下任何时候都没有问题)。

考虑到我更新的信息和查询,你能帮我解决这个问题吗?

我已经通过电子邮件收到了您的代码,并设法提取了我的一些问题的答案。

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.

我要的代码在你的 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();
}

你会发现这是一个不好的地方,因为你显然忘记了它甚至在那里。另一个不适合放置它的原因是您应该改用内置 DialogExtensions.RunAsync 方法。

发生的事情是您将自适应卡的提交操作中的序列化 JSON 传递到任何活动的对话框中。因此,如果活动对话框是一个选择提示,它将尝试将序列化的 JSON 解释为选择之一。单击取消按钮时,JSON 将包含 "Cancel": 1,而 1 使识别器认为您想使用选项 1。

最简单的解决方案当然是修改您的自适应卡,使其不包含任何数字,但这当然是一个临时修复,可能不适用于您未来的所有场景。

你实际上并没有说出你的 expected/desired 行为是什么,但我可以想到两个主要选项:

  1. 您希望在该提示之外单击提交操作时忽略它们
  2. 无论哪个对话框处于活动状态,您都希望取消按钮取消任何对话框

我可以根据您的代码猜测您可能打算让按钮仅在该提示中起作用。由于您使用的是网络聊天,您可能会考虑一个客户端解决方案,您可以在其中制作自己的自适应卡片渲染器,允许在使用卡片后禁用提交操作。我认为解决方案比您想要的更难,但也有一些方法可以让机器人在特定情况下忽略提交操作。你可以看看 Michael Richardson's Adaptive Card prompt for some ideas, and also vote up my Adaptive Cards community project.

如果您希望取消按钮适用于任何对话框,只需确保通过调用 CancelAllDialogsAsync 而不是 ContinueDialogAsync 来响应其 activity。

How is "response" generated in your main root dialog

这是我要的行:

var response = (stepContext.Result as FoundChoice)?.Value;

您莫名其妙地从 "main root dialog," 中省略了该行,但我注意到您在名称 DisplayOptionsDialog 下冗余粘贴该代码时包含了该行。如果您不遗漏重要信息,或者至少在被问及时提供,将来您将能够更快地获得更好的帮助。

请参阅 my latest blog post 了解有关将自适应卡片与 Bot Framework 一起使用的更多信息。