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();
エミュレーターで検証
件名の表示と検証の確認
Hours が表示されるか検証
メッセージをカスタマイズ
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();
最終的に以下のコードにしました。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();
}
}
}
テストの実行
前回失敗するようになったテストも、今回の変更で通るはずです。コードをチェックインして確認します。
まとめ
フォームフローの便利さと柔軟性は素晴らしいです。今回紹介した機能のほかにも、クラス側の属性として色々設定できますので是非試してください。
次回はそろそろ多言語処理でも。