diff --git a/libraries/Microsoft.Bot.Builder.Dialogs.Adaptive/Actions/ForEachElement.cs b/libraries/Microsoft.Bot.Builder.Dialogs.Adaptive/Actions/ForEachElement.cs index 87eef29fd4..24826f86ea 100644 --- a/libraries/Microsoft.Bot.Builder.Dialogs.Adaptive/Actions/ForEachElement.cs +++ b/libraries/Microsoft.Bot.Builder.Dialogs.Adaptive/Actions/ForEachElement.cs @@ -132,16 +132,25 @@ public List Actions } /// - public override async Task ContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken = default) + public override async Task OnDialogEventAsync(DialogContext dc, DialogEvent e, CancellationToken cancellationToken) { - return await RunItemsAsync(dc, beginDialog: false, cancellationToken: cancellationToken).ConfigureAwait(false); + var handled = await base.OnDialogEventAsync(dc, e, cancellationToken).ConfigureAwait(false); + + if (!handled && e?.Name == DialogEvents.RepromptDialog) + { + var childState = GetActionScopeState(dc); + var childDc = CreateChildContext(dc, childState); + await childDc.RepromptDialogAsync(cancellationToken).ConfigureAwait(false); + handled = true; + } + + return handled; } /// - public override async Task ResumeDialogAsync(DialogContext dc, DialogReason reason, object result = null, CancellationToken cancellationToken = default) + public override async Task ContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken = default) { - var res = await base.ResumeDialogAsync(dc, reason, result, cancellationToken).ConfigureAwait(false); - return res; + return await RunItemsAsync(dc, beginDialog: false, cancellationToken: cancellationToken).ConfigureAwait(false); } /// @@ -301,11 +310,10 @@ private DialogState GetActionScopeState(DialogContext dc) if (state == null) { state = new DialogState(); - } - - if (activeDialogState != null) - { - activeDialogState[ActionScopeState] = state; + if (activeDialogState != null) + { + activeDialogState[ActionScopeState] = state; + } } return state; diff --git a/tests/Microsoft.Bot.Builder.Dialogs.Adaptive.Tests/AdaptiveDialogTests.cs b/tests/Microsoft.Bot.Builder.Dialogs.Adaptive.Tests/AdaptiveDialogTests.cs index 9d56d121a7..03788c20f9 100644 --- a/tests/Microsoft.Bot.Builder.Dialogs.Adaptive.Tests/AdaptiveDialogTests.cs +++ b/tests/Microsoft.Bot.Builder.Dialogs.Adaptive.Tests/AdaptiveDialogTests.cs @@ -21,6 +21,7 @@ using AdaptiveExpressions.Properties; using Microsoft.Extensions.Configuration; using Microsoft.Bot.Builder.Dialogs.Adaptive.Testing.TestActions; +using Microsoft.Bot.Builder.Dialogs.Adaptive.Generators; namespace Microsoft.Bot.Builder.Dialogs.Adaptive.Tests { @@ -298,6 +299,231 @@ public async Task AdaptiveDialog_NestedMemoryAccess() await TestUtils.RunTestScript(_resourceExplorerFixture.ResourceExplorer); } + [Fact] + public async Task TestForEachElementReprompt() + { + var testFlow = new TestScript() + { + Dialog = new ForEachElementRepromptMainDialog() + } + .SendConversationUpdate(); + + testFlow = testFlow.AssertReply("send me some text inside the old foreach"); + testFlow.Script.Add(new UserSays() { Text = "interrupt" }); + testFlow = testFlow.AssertReply("Hello from InterruptDialog"); + testFlow = testFlow.AssertReply("send me some text inside the old foreach"); + testFlow.Script.Add(new UserSays() { Text = "message one" }); + testFlow = testFlow.AssertReply("Foreach You said: 'message one'"); + + testFlow = testFlow.AssertReply("send me some text inside the new foreach element"); + testFlow.Script.Add(new UserSays() { Text = "interrupt" }); + testFlow = testFlow.AssertReply("Hello from InterruptDialog"); + testFlow = testFlow.AssertReply("send me some text inside the new foreach element"); + testFlow.Script.Add(new UserSays() { Text = "message two" }); + testFlow = testFlow.AssertReply("ForEachElement You said: 'message two'"); + + await testFlow.ExecuteAsync(_resourceExplorerFixture.ResourceExplorer); + } + + private class ForEachElementRepromptMainDialog : ComponentDialog + { + public ForEachElementRepromptMainDialog() + : base(nameof(ForEachElementRepromptMainDialog)) + { + AddDialog(new FlowDialog()); + AddDialog(new InterruptDialog()); + } + + protected override Task OnBeginDialogAsync(DialogContext dc, object options, CancellationToken cancellationToken = default) + => ProcessTurnAsync(dc, cancellationToken); + + protected override Task OnContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken = default) + => ProcessTurnAsync(dc, cancellationToken); + + public async Task ProcessTurnAsync(DialogContext dc, CancellationToken cancellationToken = default) + { + if (dc.Context.Activity.Text == "interrupt") + { + dc.Context.Activity.Text = string.Empty; + dc.Context.Activity.Value = null; + var interuptDialogTurnResult = await dc.BeginDialogAsync(nameof(InterruptDialog), null, cancellationToken).ConfigureAwait(false); + + if (interuptDialogTurnResult.Status == DialogTurnStatus.CompleteAndWait || interuptDialogTurnResult.Status == DialogTurnStatus.Waiting) + { + return interuptDialogTurnResult; + } + } + + if (dc.ActiveDialog == null) + { + if (dc.Context.Activity.Type == ActivityTypes.ConversationUpdate) + { + var dialogTurnResult = await dc.BeginDialogAsync(nameof(FlowDialog)).ConfigureAwait(false); + + switch (dialogTurnResult.Status) + { + case DialogTurnStatus.Empty: + case DialogTurnStatus.Complete: + case DialogTurnStatus.Cancelled: + dc.Context.Activity.Text = string.Empty; + dc.Context.Activity.Value = null; + return await dc.BeginDialogAsync(nameof(FlowDialog), null, cancellationToken).ConfigureAwait(false); + default: + return dialogTurnResult; + } + } + else + { + return await dc.BeginDialogAsync(nameof(FlowDialog)); + } + } + else + { + var dialogTurnResult = await dc.ContinueDialogAsync(cancellationToken); + switch (dialogTurnResult.Status) + { + case DialogTurnStatus.Empty: + case DialogTurnStatus.Complete: + case DialogTurnStatus.Cancelled: + dc.Context.Activity.Text = string.Empty; + dc.Context.Activity.Value = null; + dialogTurnResult = await dc.BeginDialogAsync(nameof(FlowDialog), null, cancellationToken).ConfigureAwait(false); + break; + } + + return dialogTurnResult; + } + } + + private class InterruptDialog : ComponentDialog + { + public InterruptDialog() + : base(nameof(InterruptDialog)) + { + var proactiveDialog = new AdaptiveDialog(nameof(InterruptDialog)) { Generator = new TemplateEngineLanguageGenerator() }; + + proactiveDialog.Triggers.Add(new OnBeginDialog() + { + Actions = + { + new CodeAction(async (dc, obj) => + { + await dc.Context.SendActivityAsync($"Hello from {nameof(InterruptDialog)}"); + return await dc.EndDialogAsync(); + }), + } + }); + + AddDialog(proactiveDialog); + InitialDialogId = nameof(InterruptDialog); + } + } + + private class FlowDialog : ComponentDialog + { + public FlowDialog() + : base(nameof(FlowDialog)) + { + var flowDialogStart = new AdaptiveDialog(nameof(FlowDialog)) { Generator = new TemplateEngineLanguageGenerator() }; + + flowDialogStart.Triggers.Add(new OnBeginDialog() + { + Actions = + { + new CodeAction(async (dc, obj) => + { + dc.State.SetValue("$items", new List { "1" }); + return await dc.EndDialogAsync(); + }), + new Foreach + { + ItemsProperty = "$items", + Actions = new List + { + new InputDialogWithRePrompt( + new TextInput + { + Prompt = new ActivityTemplate("send me some text inside the old foreach"), + Property = "$answer", + AlwaysPrompt = true + }, + "inputtext"), + new SendActivity { Activity = new ActivityTemplate("Foreach You said: '${$answer}'") } + } + }, + new ForEachElement + { + ItemsProperty = "$items", + Actions = new List + { + new InputDialogWithRePrompt( + new TextInput + { + Prompt = new ActivityTemplate("send me some text inside the new foreach element"), + Property = "$answer", + AlwaysPrompt = true + }, + "inputtext"), + new SendActivity { Activity = new ActivityTemplate("ForEachElement You said: '${$answer}'") } + } + } + } + }); + + AddDialog(flowDialogStart); + InitialDialogId = nameof(FlowDialog); + } + } + + private class InputDialogWithRePrompt + : Dialog + where T : InputDialog + { + public InputDialogWithRePrompt(T inputDialog, string id) + { + InputDialog = inputDialog; + this.Id = id; + } + + public T InputDialog { get; } + + public override Task BeginDialogAsync(DialogContext dc, object options = null, CancellationToken cancellationToken = default) + { + this.DialogContext = dc; + return this.InputDialog.BeginDialogAsync(dc, options, cancellationToken); + } + + public override Task ContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken = default) + { + this.DialogContext = dc; + return this.InputDialog.ContinueDialogAsync(dc, cancellationToken); + } + + public override Task EndDialogAsync(ITurnContext turnContext, DialogInstance instance, DialogReason reason, CancellationToken cancellationToken = default) + { + return this.InputDialog.EndDialogAsync(turnContext, instance, reason, cancellationToken); + } + + public override Task OnDialogEventAsync(DialogContext dc, DialogEvent e, CancellationToken cancellationToken) + { + this.DialogContext = dc; + return InputDialog.OnDialogEventAsync(dc, e, cancellationToken); + } + + public override async Task RepromptDialogAsync(ITurnContext turnContext, DialogInstance instance, CancellationToken cancellationToken = default) + { + var dialogContext = this.DialogContext ?? turnContext.TurnState.Get(); + if (dialogContext != null) + { + var messageActivity = await this.InputDialog.Prompt.BindAsync(dialogContext, null, cancellationToken); + await turnContext.SendActivityAsync(messageActivity, cancellationToken); + } + } + + private DialogContext DialogContext { get; set; } + } + } + [Theory] //[InlineData(1000)]