前回はフォームフローの概要を紹介しました。今回はフォームフローの高度な使い方を見ていきます。
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(); } } }
テストの実行
前回失敗するようになったテストも、今回の変更で通るはずです。コードをチェックインして確認します。
まとめ
フォームフローの便利さと柔軟性は素晴らしいです。今回紹介した機能のほかにも、クラス側の属性として色々設定できますので是非試してください。
次回はそろそろ多言語処理でも。