Bot Framework と Microsoft Graph で DevOps その 11 : フォームフロー応用編

前回はフォームフローの概要を紹介しました。今回はフォームフローの高度な使い方を見ていきます。

FormBuilder

前回は以下のコードを使ってクラスからダイアログを自動生成しました。

return new FormBuilder<OutlookEvent>()
    .Message("イベントを作成します。")
    .AddRemainingFields() // すべてのフィールドを処理対象として追加
    .OnCompletion(processOutlookEventCreate)
    .Build();

FormBuilder は様々な機能がありますが、上記の例では AddRemainingFields メソッドで、クラスのすべてのフィールドをダイアログに追加、OnCompletion メソッドで完了時の処理呼び出しをしています。プロンプトの情報はクラスのプロパティに定義した Prompt や Template 属性から情報を得ています。

[Prompt("件名は?")]
public string Subject { get; set; }

以下で他の機能を見ていきます。

個別にフィールド追加

Field メソッドを使うことで、個別にフィールドが追加できます。例えば以下の場合は件名と詳細だけを含めています。

return new FormBuilder<OutlookEvent>()
    .Message("イベントを作成します。")
    .Field(nameof(OutlookEvent.Subject))
    .Field(nameof(OutlookEvent.Description))
    .OnCompletion(processOutlookEventCreate)
    .Build();
Field メソッドは個別にフィールドを追加するだけでなく、プロンプトの指定、表示するかどうかの検証、入力した値の検証などを含めることが出来ます。
return new FormBuilder<OutlookEvent>()
    .Message("イベントを作成します。")
    .Field(nameof(OutlookEvent.Subject), prompt:"予定の件名は?", validate: async (state, value) => 
    {
        // 入力を検証
        var subject = (string)value;
        var result = new ValidateResult() { IsValid = true, Value = subject };
        if (subject.Contains("FormFlow"))
        {
            result.IsValid = false;
            result.Feedback = "FormFlow については予定を作れません。";
        }
        return result;
    })
    .Field(nameof(OutlookEvent.Description))
    .Field(nameof(OutlookEvent.Start))
    .Field(nameof(OutlookEvent.IsAllDay))
    .Field(nameof(OutlookEvent.Hours), active: (state) =>
    {
        // 表示するかを検証
        if (state.IsAllDay)
            return false;
        else
            return true;
    })
    .OnCompletion(processOutlookEventCreate)
    .Build();

エミュレーターで検証

件名の表示と検証の確認

image

Hours が表示されるか検証

image

image

メッセージをカスタマイズ

Message メソッドでユーザーに対して返信できますが、現在の値を使いたい場合は以下のようにできます。

return new FormBuilder<OutlookEvent>()
    .Message("イベントを作成します。")
    .Field(nameof(OutlookEvent.Subject))
    .Message(async (state)=> { return new PromptAttribute($"現在の件名は{state.Subject}です。"); })
    .Field(nameof(OutlookEvent.Description))
    .OnCompletion(processOutlookEventCreate)
    .Build();

最終確認

Confirm メソッドを使うと、確認を挟むことができます。

return new FormBuilder<OutlookEvent>()
    .Message("イベントを作成します。")
    .Field(nameof(OutlookEvent.Subject), prompt: "予定の件名は?", validate: async (state, value) =>
        {
        // 入力を検証
        var subject = (string)value;
            var result = new ValidateResult() { IsValid = true, Value = subject };
            if (subject.Contains("FormFlow"))
            {
                result.IsValid = false;
                result.Feedback = "FormFlow については予定を作れません。";
            }
            return result;
        })
    .Field(nameof(OutlookEvent.Description))
    .Field(nameof(OutlookEvent.Start))
    .Field(nameof(OutlookEvent.IsAllDay))
    .Field(nameof(OutlookEvent.Hours), active: (state) =>
    {
        // 表示するかを検証
        if (state.IsAllDay)
            return false;
        else
            return true;
    })
    .Confirm(async (state) => 
    {
        if (state.IsAllDay)
            return new PromptAttribute("終日イベントでいいですか?");
        else
            return new PromptAttribute($"イベントは{state.Hours}時間でいいですか?");
    })
    .OnCompletion(processOutlookEventCreate)
    .Build();

エミュレーターで検証

image

最終的に以下のコードにしました。CreateEventDialog.cs を以下コードと差し替えます。

using Autofac;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.FormFlow;
using Microsoft.Graph;
using O365Bot.Models;
using O365Bot.Services;
using System;
using System.Threading.Tasks;

namespace O365Bot.Dialogs
{
    [Serializable]
    public class CreateEventDialog : IDialog<bool> // このダイアログが完了時に返す型
    {
        public async Task StartAsync(IDialogContext context)
        {
            // FormFlow でダイアログを作成して、呼び出し。
            var outlookEventFormDialog = FormDialog.FromForm(this.BuildOutlookEventForm, FormOptions.PromptInStart);
            context.Call(outlookEventFormDialog, this.ResumeAfterDialog);
        }

        private async Task ResumeAfterDialog(IDialogContext context, IAwaitable<OutlookEvent> result)
        {
            await context.PostAsync("イベントを作成しました。");

            // ダイアログの完了を宣言
            context.Done(true);
        }

        private IForm<OutlookEvent> BuildOutlookEventForm()
        {
            OnCompletionAsyncDelegate<OutlookEvent> processOutlookEventCreate = async (context, state) =>
            {
                using (var scope = WebApiApplication.Container.BeginLifetimeScope())
                {
                    IEventService service = scope.Resolve<IEventService>(new TypedParameter(typeof(IDialogContext), context));
                    // TimeZone は https://graph.microsoft.com/beta/me/mailboxSettings で取得可能だがここでは一旦ハードコード
                    Event @event = new Event()
                    {
                        Subject = state.Subject,
                        Start = new DateTimeTimeZone() { DateTime = state.Start.ToString(), TimeZone = "Tokyo Standard Time" },
                        IsAllDay = state.IsAllDay,
                        End = state.IsAllDay ? null : new DateTimeTimeZone() { DateTime = state.Start.AddHours(state.Hours).ToString(), TimeZone = "Tokyo Standard Time" },
                        Body = new ItemBody() { Content = state.Description, ContentType = BodyType.Text }
                    };
                    await service.CreateEvent(@event);
                }
            };

            return new FormBuilder<OutlookEvent>()
                .Message("イベントを作成します。")
                .Field(nameof(OutlookEvent.Subject))
                .Field(nameof(OutlookEvent.Description))
                .Field(nameof(OutlookEvent.Start))
                .Field(nameof(OutlookEvent.IsAllDay))
                .Field(nameof(OutlookEvent.Hours), active: (state) =>
                {
                    // 表示するかを検証
                    if (state.IsAllDay)
                        return false;
                    else
                        return true;
                })
                .OnCompletion(processOutlookEventCreate)
                .Build();
        }        
    }
}

テストの実行

前回失敗するようになったテストも、今回の変更で通るはずです。コードをチェックインして確認します。

まとめ

フォームフローの便利さと柔軟性は素晴らしいです。今回紹介した機能のほかにも、クラス側の属性として色々設定できますので是非試してください。

次回はそろそろ多言語処理でも。