Bot Builder v4 でボット開発 : ダイアログから高度なアダプティブカードを送る

前回は基本的なアダプティブカードを使ってダイアログを作成しました。今回はより高度で複雑なものを試していきます。

# アダプティブカードの要素

色々試す前に、まずアダプティブカードで出来ることを見ていきます。詳細は [Card Schema](https://docs.microsoft.com/en-us/adaptive-cards/authoring-cards/card-schema) を参照。

アダプティブカードには以下の要素があります。

- Card Elements - テキストや画像などカード上に表示される要素
- Containers - 複数の Card Elements を 1 つにまとめる箱
- Actions - ボタンクリック時の動作
- Inputs - データを入力するための機能
- Speech - 音声用の情報
 
## Card Elements

- TextBlock - テキストの表示。フォントのサイズや色、スタイルを設定できる
- Image - 画像の表示
- Media - 動画や音声の再生ができる
    - MediaSource - メディアのソース定義
   
### テキストのマークダウン

太字やイタリック、ブレットポイントなどの書式をマークダウンで指定可能

## Containers

- Container - 複数のアイテムをグループ化
- ColumnSet - 複数の列を作成し、横並びに表示
    - Column - ColumnSet で使う 1 列の定義
- FactSet - キー/値ペアをテーブル形式で表示
    - Fact - FactSet で使う 1 つのキー/値の定義
- ImageSet - 複数イメージのグループ化

## Actions

- Action.OpenUrl - 指定した URL を開く
- Action.Submit - Inputs で指定したデータを集めて送る
- Action.ShowCard - 定義したアダプティブカードを開く

## Inputs

- Input.Text - テキストの入力。メールアドレスや URL などスタイルの指定や既定値などが設定可能
- Input.Number - 数値の入力
- Input.Date - 日付の入力
- Input.Time - 時間の入力
- Input.Toggle - 2 つのオプションのトグル
- Input.ChoiceSet - 選択肢の提示
    - Input.Choice - ChoiseSet で使う選択肢の定義


# プロファイルダイアログの拡張

前回プロファイルダイアログをアダプティブカード化しましたが、内容を拡張してみます。

1\. まず Models フォルダの UserProfile.cs を以下の様に変更。追加の情報を持てるようにする。

```csharp
using System;
using System.Collections.Generic;

public class UserProfile
{
    public string Name { get; set; }
    public int Age { get; set; }
    public DateTime BirthDay { get; set; }
    public string Email { get; set; }
    public string Phone { get; set; }
    public string CatNum { get; set; }
    public List<string> CatTypes { get; set; }
    public bool HasCat { get; set; }
    public bool PlayWithCat { get; set; }
}
```

2\. 次に Profile.json を以下の様に変更。

- Input.ChoiceSet を複数の方法で利用
- Input.Text のスタイルを複数利用

```json
{
    "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
    "type": "AdaptiveCard",
    "version": "1.0",
    "body": [
        {
            "type": "ColumnSet",
            "columns": [
                {
                    "type": "Column",
                    "width": 2,
                    "items": [   
                        {
                            "type": "TextBlock",
                            "text": "プロファイルを登録します。",
                            "weight": "bolder",
                            "size": "medium"
                        },
                        {
                            "type": "TextBlock",
                            "text": "ボットの利用にあたり、もう少し詳しくあなたの情報が必要です。",
                            "isSubtle": true,
                            "wrap": true
                        },
                        {
                            "type": "TextBlock",
                            "text": "名前",
                            "wrap": true
                        },
                        {
                            "type": "Input.Text",
                            "id": "name",
                            "placeholder": "姓名",
                            "style": "text"
                        },
                        {
                            "type": "TextBlock",
                            "text": "誕生日",
                            "wrap": true
                        },
                        {
                            "type": "Input.Date",
                            "id": "birthday"
                        },
                        {
                            "type": "TextBlock",
                            "text": "メールアドレス",
                            "wrap": true
                        },
                        {
                            "type": "Input.Text",
                            "id": "email",
                            "placeholder": "youremail@example.com",
                            "style": "email"
                        },
                        {
                            "type": "TextBlock",
                            "text": "電話番号"
                        },
                        {
                            "type": "Input.Text",
                            "id": "phone",
                            "placeholder": "(xx)-xxxx-xxxx",
                            "style": "tel"
                        },
                        {
                            "type": "TextBlock",
                            "text": "ネコ飼っていますか？"
                        },
                        {
                            "type": "Input.ChoiceSet",
                            "id": "hasCat",
                            "style": "expanded",
                            "value":"true",
                            "choices": [
                                {
                                    "title": "はい",
                                    "value": "true"
                                },
                                {
                                    "title": "いいえ",
                                    "value": "false"
                                }
                            ]
                        },
                        {
                            "type": "TextBlock",
                            "text": "飼っているネコの種類を教えてください。(複数選択可)"
                        },
                        {
                            "type": "Input.ChoiceSet",
                            "id": "catTypes",
                            "isMultiSelect": true,
                            "choices": [
                                {
                                    "title": "サバトラ",
                                    "value": "サバトラ"
                                },
                                {
                                    "title": "キジトラ",
                                    "value": "キジトラ"
                                },
                                {
                                    "title": "ハチワレ",
                                    "value": "ハチワレ"
                                },
                                {
                                    "title": "その他",
                                    "value": "その他"
                                }
                            ]
                        },
                        {
                            "type": "TextBlock",
                            "text": "全部で何匹いますか？"
                        },
                        {
                            "type": "Input.ChoiceSet",
                            "id": "catNum",
                            "style": "compact",
                            "value": "1",
                            "choices": [
                                {
                                    "title": "1",
                                    "value": "1"
                                },
                                {
                                    "title": "2",
                                    "value": "2"
                                },
                                {
                                    "title": "3",
                                    "value": "3"
                                },
                                {
                                    "title": "4",
                                    "value": "4"
                                },
                                {
                                    "title": "5",
                                    "value": "5"
                                },
                                {
                                    "title": "6匹以上",
                                    "value": "6+"
                                }
                            ]
                        },
                        {
                            "type": "TextBlock",
                            "text": "毎日ネコと遊んでいますか？"
                        },
                        {
                            "type": "Input.Toggle",
                            "title": "毎日ネコを遊んでいます。",
                            "valueOn": "true",
                            "valueOff": "false",
                            "id": "playWithCat"
                        }
                    ]
                },
                {
                    "type": "Column",
                    "width": 1,
                    "items": [
                        {
                            "type": "Image",
                            "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/1/15/Pumiforme.JPG/1280px-Pumiforme.JPG",
                            "size": "auto"
                        }
                    ]
                }
            ]
        }
    ],
    "actions": [
        {
            "type": "Action.Submit",
            "title": "登録"
        }
    ]
}
```

3\. AdaptiveCardResponseValidators.cs の ValidateInput メソッドを以下の様に変更。

```csharp
public static Task<bool> ValidateInput(PromptValidatorContext<string> promptContext, CancellationToken cancellationToken)
{
    // 文字は返ってこないため Succeeded の場合は false
    if (promptContext.Recognized.Succeeded)
    {
        return Task.FromResult(false);
    }

    // オプションの検証プロパティから NumberRange を取得。
    NumberRange range = promptContext.Options.Validations is NumberRange ?
        (NumberRange)promptContext.Options.Validations :
        new NumberRange() { MinValue = 0, MaxValue = 120 };

    // 一旦 JObject として中身をパース
    var input = JObject.Parse(promptContext.Context.Activity.Value.ToString());
    // CatTypes がある場合、対応する UserProfile クラスのプロパティが 、
    // List<string> のため、JArray に変換。
    if(input.ContainsKey("catTypes"))
        input["catTypes"] = new JArray(input["catTypes"].ToString().Split(','));

    // 誕生日から年齢を計算し新しいプロパティとして追加
    var birthday = DateTime.Parse(input["birthday"].ToString());
    var age = DateTime.Now.Year - birthday.Year;
    if (DateTime.Now < birthday.AddYears(age)) 
        age--;
    input["age"] = age;
    // 0 より小さいか 120 より大きい場合は False
    if (age < range.MinValue ||age > range.MaxValue)
    {
        promptContext.Context.SendActivityAsync($"年齢が{age}歳になります。ただしい誕生日を入れてください。");
        return Task.FromResult(false);
    }
    // 値を入れ替え
    promptContext.Context.Activity.Value = input;
    return Task.FromResult(true);
}
```

4\. ProfileDialog.cs の SummaryStepAsync をシンプルに変更。

```csharp
private async Task<DialogTurnResult> SummaryStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
    var input = JsonConvert.DeserializeObject<UserProfile>(stepContext.Context.Activity.Value.ToString());
    await stepContext.Context.SendActivityAsync(MessageFactory.Text("プロファイルを保存します。"));
    await accessors.UserProfile.SetAsync(stepContext.Context, input, cancellationToken);
    return await stepContext.EndDialogAsync(cancellationToken: cancellationToken);
}
```

5\. 挨拶などにペットの情報を適当に入れてみる。

```csharp
await turnContext.SendActivityAsync(MessageFactory.Text($"ようこそ '{userProfile.Name}' さん！"));
if (userProfile.HasCat)
    await turnContext.SendActivityAsync(MessageFactory.Text($"{userProfile.CatNum}匹の猫は元気ですか？"));
```

## テスト

1\. F5 キーを押下してデバッグ実行。エミュレーターで接続。「はい」をクリック。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/5ca293c7-8e6d-6ee3-6252-4a1aa71acfe5.png)

2\. アダプティブカードの中身を埋めて、「登録」をクリック。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/41328741-0253-080a-6405-1947cac5f7b3.png)

3\. 意図した通りプロファイルが取得できることを確認。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/50c53c97-d908-f9d3-55dc-60bea70724d9.png)

4\. Restart conversation をクリックして挨拶が変わることを確認。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/b7cc00ec-050e-ed2f-6ba1-2965e6db3f73.png)

# もっと色々ためす

アダプティブカードはもっと沢山の事が出来ます。以下のサンプル集の JSON も試してください。ボタンをクリックした際に追加で内容を表示するなど、色々できます。
[アダプティブカード : サンプル](https://adaptivecards.io/samples/)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/214116/be324f02-2ba4-e1ad-5738-7e474ca90cbc.png)

# C# でアダプティブカード

C# ですべてを書きたい場合は、AdaptiveCards NuGet パッケージを使います。
[GitHub : AdaptiveCards](https://github.com/Microsoft/AdaptiveCards)
ただしこの記事を書いた時点では[問題](https://github.com/Microsoft/AdaptiveCards/issues/2148)があり、うまく動作しません。どうしても C# で書きたい場合は古いライブラリですが、Microsoft.AdaptiveCards NuGet パッケージを使ってください。

# まとめ

アダプティブカードを使うと表現の自由が上がるだけでなく、入力フォームとしてまとめて質問する事もできるため、是非色々試してください。次回はボットの多言語サポートを見ていきます。

[次の記事へ](https://qiita.com/kenakamu/items/c824a29646db58f9f55b)
[目次へ戻る](https://qiita.com/kenakamu/items/6dc043cfc1f199032883)

[この記事のサンプルコード](https://github.com/kenakamu/botbuilderv4completeguide/tree/article14)