• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

microsoft / botbuilder-dotnet / 351887

16 May 2023 06:37PM UTC coverage: 78.983% (-0.03%) from 79.011%
351887

push

CI-PR build

Microsoft.VisualStudio.Services.TFS
[#6086] Teams is adding support for suggested actions in 1-1 chats (#6607)

25915 of 32811 relevant lines covered (78.98%)

0.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.39
/libraries/Microsoft.Bot.Builder.Dialogs.Adaptive/Input/InputDialog.cs
1
// Licensed under the MIT License.
2
// Copyright (c) Microsoft Corporation. All rights reserved.
3

4
using System;
5
using System.Collections.Generic;
6
using System.Linq;
7
using System.Threading;
8
using System.Threading.Tasks;
9
using AdaptiveExpressions;
10
using AdaptiveExpressions.Properties;
11
using Microsoft.Bot.Builder.Dialogs.Choices;
12
using Microsoft.Bot.Schema;
13
using Newtonsoft.Json;
14

15
namespace Microsoft.Bot.Builder.Dialogs.Adaptive.Input
16
{
17
    /// <summary>
18
    /// Defines input dialogs.
19
    /// </summary>
20
    public abstract class InputDialog : Dialog
21
    {
22
#pragma warning disable SA1310 // Field should not contain underscore.
23
#pragma warning disable CA1707 // Identifiers should not contain underscores (we shouldn't use screaming caps, but we can't change this without breaking binary compat)
24
        /// <summary>
25
        /// Defines dialog context turn count property value.
26
        /// </summary>
27
        protected const string TURN_COUNT_PROPERTY = "this.turnCount";
28

29
        /// <summary>
30
        /// Defines dialog context state property value.
31
        /// </summary>
32
        protected const string VALUE_PROPERTY = "this.value";
33
#pragma warning restore CA1707 // Identifiers should not contain underscores
34
#pragma warning restore SA1310 // Field should not contain underscore.
35

36
        private readonly JsonSerializerSettings _settings = new JsonSerializerSettings { MaxDepth = null };
1✔
37

38
        /// <summary>
39
        /// Gets or sets a value indicating whether the input should always prompt the user regardless of there being a value or not.
40
        /// </summary>
41
        /// <value>
42
        /// Bool or expression which evaluates to bool.
43
        /// </value>
44
        [JsonProperty("alwaysPrompt")]
45
        public BoolExpression AlwaysPrompt { get; set; }
1✔
46

47
        /// <summary>
48
        /// Gets or sets intteruption policy. 
49
        /// </summary>
50
        /// <value>
51
        /// Bool or expression which evalutes to bool.
52
        /// </value>
53
        [JsonProperty("allowInterruptions")]
54
        public BoolExpression AllowInterruptions { get; set; }
1✔
55

56
        /// <summary>
57
        /// Gets or sets whether this action should be disabled.
58
        /// </summary>
59
        /// <remarks>If this is set to true, then this action will be skipped.</remarks>
60
        /// <value>
61
        /// Bool or expression which evalutes to bool.
62
        /// </value>
63
        [JsonProperty("disabled")]
64
        public BoolExpression Disabled { get; set; }
1✔
65

66
        /// <summary>
67
        /// Gets or sets the memory property path which the value will be bound to.
68
        /// </summary>
69
        /// <remarks>
70
        /// This property will be used as the initial value for the input dialog.  The result of this
71
        /// dialog will be placed into this property path in the callers memory scope.
72
        /// </remarks>
73
        /// <value>
74
        /// A string or expression which evaluates to string.
75
        /// </value>
76
        [JsonProperty("property")]
77
        public StringExpression Property { get; set; }
1✔
78

79
        /// <summary>
80
        /// Gets or sets a the expression to use to bind input to the dialog.  
81
        /// </summary>
82
        /// <remarks>
83
        /// This expression is evaluated on every turn to define mapping user input to the dialog. 
84
        /// 
85
        /// If the expression returns null, the input dialog may attempt to pull data from the input directly.
86
        /// 
87
        /// If the expression is a value then it will be used as the input.
88
        /// 
89
        /// This property allows you to define how data such as Recognizer results is bound to the input dialog.
90
        /// Examples:
91
        /// * "=@age" => bind to the input to any age entity recognized in the input. 
92
        /// * "=coalesce(@age, @number)" => which means use @age or @number as the input.
93
        /// </remarks>
94
        /// <value>
95
        /// An expression which is evaluated to define the input value.
96
        /// </value>
97
        [JsonProperty("value")]
98
        public ValueExpression Value { get; set; }
1✔
99

100
        /// <summary>
101
        /// Gets or sets the activity to send to the user.
102
        /// </summary>
103
        /// <value>
104
        /// An activity template.
105
        /// </value>
106
        [JsonProperty("prompt")]
107
        public ITemplate<Activity> Prompt { get; set; }
1✔
108

109
        /// <summary>
110
        /// Gets or sets the activity template for retrying.
111
        /// </summary>
112
        /// <value>
113
        /// An activity template.
114
        /// </value>
115
        [JsonProperty("unrecognizedPrompt")]
116
        public ITemplate<Activity> UnrecognizedPrompt { get; set; }
1✔
117

118
        /// <summary>
119
        /// Gets or sets the activity template to send to the user whenever the value provided is invalid.
120
        /// </summary>
121
        /// <value>
122
        /// An activity template.
123
        /// </value>
124
        [JsonProperty("invalidPrompt")]
125
        public ITemplate<Activity> InvalidPrompt { get; set; }
1✔
126

127
        /// <summary>
128
        /// Gets or sets the activity template to send when MaxTurnCount has been reached and the default value is used.
129
        /// </summary>
130
        /// <value>
131
        /// An activity template.
132
        /// </value>
133
        [JsonProperty("defaultValueResponse")]
134
        public ITemplate<Activity> DefaultValueResponse { get; set; }
1✔
135

136
        /// <summary>
137
        /// Gets or sets the expressions to run to validate the input.
138
        /// </summary>
139
        /// <value>
140
        /// The expressions to run to validate the input.
141
        /// </value>
142
        [JsonProperty("validations")]
143
#pragma warning disable CA2227 // Collection properties should be read only (we can't change this without breaking binary compat)
144
        public List<BoolExpression> Validations { get; set; } = new List<BoolExpression>();
1✔
145
#pragma warning restore CA2227 // Collection properties should be read only
146

147
        /// <summary>
148
        /// Gets or sets maximum number of times to ask the user for this value before the dialog gives up.
149
        /// </summary>
150
        /// <value>
151
        /// Integer or expression which evaluates to integer.
152
        /// </value>
153
        [JsonProperty("maxTurnCount")]
154
        public IntExpression MaxTurnCount { get; set; }
1✔
155

156
        /// <summary>
157
        /// Gets or sets the default value for the input dialog when MaxTurnCount is exceeded.
158
        /// </summary>
159
        /// <value>
160
        /// Value or expression which evaluates to a value.
161
        /// </value>
162
        [JsonProperty("defaultValue")]
163
        public ValueExpression DefaultValue { get; set; }
1✔
164

165
        /// <summary>
166
        /// Called when the dialog is started and pushed onto the dialog stack.
167
        /// </summary>
168
        /// <param name="dc">The <see cref="DialogContext"/> for the current turn of conversation.</param>
169
        /// <param name="options">Optional, initial information to pass to the dialog.</param>
170
        /// <param name="cancellationToken">Optional, a <see cref="CancellationToken"/> that can be used by other objects
171
        /// or threads to receive notice of cancellation.</param>
172
        /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
173
        public override async Task<DialogTurnResult> BeginDialogAsync(DialogContext dc, object options, CancellationToken cancellationToken = default(CancellationToken))
174
        {
175
            if (dc == null)
1✔
176
            {
177
                throw new ArgumentNullException(nameof(dc));
×
178
            }
179

180
            if (options is CancellationToken)
1✔
181
            {
182
                throw new ArgumentException($"{nameof(options)} cannot be a cancellation token");
×
183
            }
184

185
            if (Disabled != null && Disabled.GetValue(dc.State))
1✔
186
            {
187
                return await dc.EndDialogAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
×
188
            }
189

190
            var op = OnInitializeOptions(dc, options);
1✔
191
            dc.State.SetValue(ThisPath.Options, op);
1✔
192
            dc.State.SetValue(TURN_COUNT_PROPERTY, 0);
1✔
193

194
            var alwaysPrompt = this.AlwaysPrompt?.GetValue(dc.State) ?? false;
1✔
195

196
            // If AlwaysPrompt is set to true, then clear Property value for turn 0.
197
            var property = this.Property?.GetValue(dc.State);
1✔
198
            if (property != null && alwaysPrompt)
1✔
199
            {
200
                dc.State.SetValue(property, null);
1✔
201
            }
202

203
            var state = alwaysPrompt ? InputState.Missing : await this.RecognizeInputAsync(dc, 0, cancellationToken).ConfigureAwait(false);
1✔
204
            if (state == InputState.Valid)
1✔
205
            {
206
                var input = dc.State.GetValue<object>(VALUE_PROPERTY);
1✔
207

208
                // set property
209
                dc.State.SetValue(property, input);
1✔
210

211
                // return as result too
212
                return await dc.EndDialogAsync(input, cancellationToken: cancellationToken).ConfigureAwait(false);
1✔
213
            }
214
            else
215
            {
216
                // turnCount should increase here, because you want when nextTurn comes in
217
                // We will set the turn count to 1 so the input will not pick from "dialog.value"
218
                // and instead go with "turn.activity.text"
219
                dc.State.SetValue(TURN_COUNT_PROPERTY, 1);
1✔
220
                return await this.PromptUserAsync(dc, state, cancellationToken).ConfigureAwait(false);
1✔
221
            }
222
        }
1✔
223

224
        /// <summary>
225
        /// Called when the dialog is _continued_, where it is the active dialog and the
226
        /// user replies with a new activity.
227
        /// </summary>
228
        /// <param name="dc">The <see cref="DialogContext"/> for the current turn of conversation.</param>
229
        /// <param name="cancellationToken">Optional, a <see cref="CancellationToken"/> that can be used by other objects
230
        /// or threads to receive notice of cancellation.</param>
231
        /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
232
        public override async Task<DialogTurnResult> ContinueDialogAsync(DialogContext dc, CancellationToken cancellationToken = default(CancellationToken))
233
        {
234
            var activity = dc.Context.Activity;
1✔
235

236
            // Interrupted dialogs reprompt so we can ignore the incoming activity. 
237
            var interrupted = dc.State.GetValue<bool>(TurnPath.Interrupted, () => false);
1✔
238
            if (!interrupted && activity.Type != ActivityTypes.Message)
1✔
239
            {
240
                return EndOfTurn;
1✔
241
            }
242

243
            var turnCount = dc.State.GetValue<int>(TURN_COUNT_PROPERTY, () => 0);
1✔
244

245
            // Perform base recognition
246
            var state = await RecognizeInputAsync(dc, interrupted ? 0 : turnCount, cancellationToken).ConfigureAwait(false);
1✔
247

248
            if (state == InputState.Valid)
1✔
249
            {
250
                var input = dc.State.GetValue<object>(VALUE_PROPERTY);
1✔
251

252
                // set output property
253
                if (this.Property != null)
1✔
254
                {
255
                    dc.State.SetValue(this.Property.GetValue(dc.State), input);
1✔
256
                }
257

258
                return await dc.EndDialogAsync(input, cancellationToken: cancellationToken).ConfigureAwait(false);
1✔
259
            }
260
            else if (this.MaxTurnCount == null || turnCount < this.MaxTurnCount.GetValue(dc.State))
1✔
261
            {
262
                // increase the turnCount as last step
263
                if (!interrupted)
1✔
264
                {
265
                    dc.State.SetValue(TURN_COUNT_PROPERTY, turnCount + 1);
1✔
266
                }
267

268
                return await this.PromptUserAsync(dc, state, cancellationToken).ConfigureAwait(false);
1✔
269
            }
270
            else
271
            {
272
                if (this.DefaultValue != null)
1✔
273
                {
274
                    var (value, error) = this.DefaultValue.TryGetValue(dc.State);
1✔
275
                    if (this.DefaultValueResponse != null)
1✔
276
                    {
277
                        var response = await this.DefaultValueResponse.BindAsync(dc, cancellationToken: cancellationToken).ConfigureAwait(false);
1✔
278

279
                        var properties = new Dictionary<string, string>()
1✔
280
                        {
1✔
281
                            { "template", JsonConvert.SerializeObject(DefaultValueResponse, _settings) },
1✔
282
                            { "result", response == null ? string.Empty : JsonConvert.SerializeObject(response, new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore, MaxDepth = null }) },
1✔
283
                            { "context", TelemetryLoggerConstants.InputDialogResultEvent }
1✔
284
                        };
1✔
285
                        TelemetryClient.TrackEvent(TelemetryLoggerConstants.GeneratorResultEvent, properties);
1✔
286
                        if (response != null)
1✔
287
                        {
288
                            await dc.Context.SendActivityAsync(response, cancellationToken).ConfigureAwait(false);
1✔
289
                        }
290
                    }
291

292
                    // set output property
293
                    dc.State.SetValue(this.Property.GetValue(dc.State), value);
1✔
294

295
                    return await dc.EndDialogAsync(value, cancellationToken).ConfigureAwait(false);
1✔
296
                }
297
            }
298

299
            return await dc.EndDialogAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
1✔
300
        }
1✔
301

302
        /// <summary>
303
        /// Called when a child dialog completes its turn, returning control to this dialog.
304
        /// </summary>
305
        /// <param name="dc">The <see cref="DialogContext"/> for the current turn of conversation.</param>
306
        /// <param name="reason">Reason why the dialog resumed.</param>
307
        /// <param name="result">Optional, value returned from the dialog that was called. The type
308
        /// of the value returned is dependent on the child dialog.</param>
309
        /// <param name="cancellationToken">Optional, a <see cref="CancellationToken"/> that can be used by other objects
310
        /// or threads to receive notice of cancellation.</param>
311
        /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
312
        public override async Task<DialogTurnResult> ResumeDialogAsync(DialogContext dc, DialogReason reason, object result = null, CancellationToken cancellationToken = default(CancellationToken))
313
        {
314
            return await this.PromptUserAsync(dc, InputState.Missing, cancellationToken).ConfigureAwait(false);
×
315
        }
×
316

317
        /// <summary>
318
        /// Track GeneratorResultEvent telemetry event with InputDialogResultEvent context.
319
        /// </summary>
320
        /// <param name="dc">Current <see cref="DialogContext"/>.</param>
321
        /// <param name="activityTemplate"><see cref="ITemplate{T}"/> used to create the Activity.</param>
322
        /// <param name="msg">The <see cref="IMessageActivity"/> which will be sent.</param>
323
        internal virtual void TrackGeneratorResultEvent(DialogContext dc, ITemplate<Activity> activityTemplate, IMessageActivity msg)
324
        {
325
            var properties = new Dictionary<string, string>()
1✔
326
            {
1✔
327
                { "template", JsonConvert.SerializeObject(activityTemplate, _settings) },
1✔
328
                { "result", msg == null ? string.Empty : JsonConvert.SerializeObject(msg, new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Ignore, MaxDepth = null }) },
1✔
329
                { "context", TelemetryLoggerConstants.InputDialogResultEvent }
1✔
330
            };
1✔
331
            TelemetryClient.TrackEvent(TelemetryLoggerConstants.GeneratorResultEvent, properties);
1✔
332
        }
1✔
333

334
        /// <summary>
335
        /// Called when input has been received, override this method to customize recognition of the input.
336
        /// </summary>
337
        /// <param name="dc">dialogContext.</param>
338
        /// <param name="cancellationToken">the <see cref="CancellationToken"/> for the task.</param>
339
        /// <returns>InputState which reflects whether input was recognized as valid or not.</returns>
340
        protected abstract Task<InputState> OnRecognizeInputAsync(DialogContext dc, CancellationToken cancellationToken);
341

342
        /// <summary>
343
        /// Called before an event is bubbled to its parent.
344
        /// </summary>
345
        /// <param name="dc">The <see cref="DialogContext"/> for the current turn of conversation.</param>
346
        /// <param name="e">The event being raised.</param>
347
        /// <param name="cancellationToken">Optional, the <see cref="CancellationToken"/> that can be used by other objects or threads to receive notice of cancellation.</param>
348
        /// <returns>Whether the event is handled by the current dialog and further processing should stop.</returns>
349
        protected override async Task<bool> OnPreBubbleEventAsync(DialogContext dc, DialogEvent e, CancellationToken cancellationToken)
350
        {
351
            if (e.Name == DialogEvents.ActivityReceived && dc.Context.Activity.Type == ActivityTypes.Message)
1✔
352
            {
353
                // Ask parent to perform recognition
354
                await dc.Parent.EmitEventAsync(AdaptiveEvents.RecognizeUtterance, value: dc.Context.Activity, bubble: false, cancellationToken: cancellationToken).ConfigureAwait(false);
1✔
355

356
                // Should we allow interruptions
357
                var canInterrupt = true;
1✔
358
                if (this.AllowInterruptions != null)
1✔
359
                {
360
                    var (allowInterruptions, error) = this.AllowInterruptions.TryGetValue(dc.State);
1✔
361
                    canInterrupt = error == null && allowInterruptions;
1✔
362
                }
363

364
                // Stop bubbling if interruptions ar NOT allowed
365
                return !canInterrupt;
1✔
366
            }
367

368
            return false;
1✔
369
        }
1✔
370

371
        /// <summary>
372
        /// AppendChoices is utility method to build up a message activity given all of the options.
373
        /// </summary>
374
        /// <param name="prompt">prompt.</param>
375
        /// <param name="channelId">channelId.</param>
376
        /// <param name="choices">choices to present.</param>
377
        /// <param name="style">listType.</param>
378
        /// <param name="options">options to control the choice rendering.</param>
379
        /// <param name="cancellationToken">cancellation Token.</param>
380
        /// <returns>bound activity ready to send to the user.</returns>
381
        protected virtual IMessageActivity AppendChoices(IMessageActivity prompt, string channelId, IList<Choice> choices, ListStyle style, ChoiceFactoryOptions options = null, CancellationToken cancellationToken = default(CancellationToken))
382
        {
383
            return AppendChoices(prompt, channelId, choices, style, options, null, null, cancellationToken);
×
384
        }
385

386
        /// <summary>
387
        /// AppendChoices is utility method to build up a message activity given all of the options.
388
        /// </summary>
389
        /// <param name="prompt">prompt.</param>
390
        /// <param name="channelId">channelId.</param>
391
        /// <param name="choices">choices to present.</param>
392
        /// <param name="style">listType.</param>
393
        /// <param name="options">options to control the choice rendering.</param>
394
        /// <param name="conversationType">the type of the conversation.</param>
395
        /// <param name="toList">the list of recipients.</param>
396
        /// <param name="cancellationToken">cancellation Token.</param>
397
        /// <returns>bound activity ready to send to the user.</returns>
398
        protected virtual IMessageActivity AppendChoices(IMessageActivity prompt, string channelId, IList<Choice> choices, ListStyle style, ChoiceFactoryOptions options = null, string conversationType = default, IList<string> toList = default, CancellationToken cancellationToken = default(CancellationToken))
399
        {
400
            // Get base prompt text (if any)
401
            var text = prompt != null && !string.IsNullOrEmpty(prompt.Text) ? prompt.Text : string.Empty;
×
402

403
            // Create temporary msg
404
            IMessageActivity msg;
405
            switch (style)
406
            {
407
                case ListStyle.Inline:
408
                    msg = ChoiceFactory.Inline(choices, text, null, options);
1✔
409
                    break;
1✔
410

411
                case ListStyle.List:
412
                    msg = ChoiceFactory.List(choices, text, null, options);
1✔
413
                    break;
1✔
414

415
                case ListStyle.SuggestedAction:
416
                    msg = ChoiceFactory.SuggestedAction(choices, text, null, toList);
×
417
                    break;
×
418

419
                case ListStyle.HeroCard:
420
                    msg = ChoiceFactory.HeroCard(choices, text);
×
421
                    break;
×
422

423
                case ListStyle.None:
424
                    msg = Activity.CreateMessageActivity();
×
425
                    msg.Text = text;
×
426
                    break;
×
427

428
                default:
429
                    msg = ChoiceFactory.ForChannel(channelId, choices, text, null, options, conversationType, toList);
1✔
430
                    break;
431
            }
432

433
            // Update prompt with text, actions and attachments
434
            if (prompt != null)
1✔
435
            {
436
                // clone the prompt the set in the options (note ActivityEx has Properties so this is the safest mechanism)
437
                prompt = JsonConvert.DeserializeObject<Activity>(JsonConvert.SerializeObject(prompt, _settings), _settings);
1✔
438

439
                prompt.Text = msg.Text;
1✔
440

441
                if (msg.SuggestedActions != null && msg.SuggestedActions.Actions != null && msg.SuggestedActions.Actions.Count > 0)
×
442
                {
443
                    prompt.SuggestedActions = msg.SuggestedActions;
×
444
                }
445

446
                if (msg.Attachments != null && msg.Attachments.Any())
1✔
447
                {
448
                    prompt.Attachments = msg.Attachments;
×
449
                }
450

451
                return prompt;
1✔
452
            }
453
            else
454
            {
455
                msg.InputHint = InputHints.ExpectingInput;
×
456
                return msg;
×
457
            }
458
        }
459

460
        /// <summary>
461
        /// Method which processes options.
462
        /// </summary>
463
        /// <remarks>Override this method to inject additional option.</remarks>
464
        /// <param name="dc">dialogContext.</param>
465
        /// <param name="options">options.</param>
466
        /// <returns>modified options.</returns>
467
        protected virtual object OnInitializeOptions(DialogContext dc, object options)
468
        {
469
            return options;
1✔
470
        }
471

472
        /// <summary>
473
        /// Method which renders the prompt to the user give n the current input state.
474
        /// </summary>
475
        /// <remarks>Override this to customize the output sent to the user.</remarks>
476
        /// <param name="dc">dialogcontext.</param>
477
        /// <param name="state">inputState.</param>
478
        /// <param name="cancellationToken">the <see cref="CancellationToken"/> for the task.</param>
479
        /// <returns>activity to send to the user.</returns>
480
        protected virtual async Task<IActivity> OnRenderPromptAsync(DialogContext dc, InputState state, CancellationToken cancellationToken = default(CancellationToken))
481
        {
482
            IMessageActivity msg = null;
1✔
483
            ITemplate<Activity> template = null;
1✔
484
            switch (state)
485
            {
486
                case InputState.Unrecognized:
487
                    if (this.UnrecognizedPrompt != null)
1✔
488
                    {
489
                        template = this.UnrecognizedPrompt;
1✔
490
                        msg = await this.UnrecognizedPrompt.BindAsync(dc, cancellationToken: cancellationToken).ConfigureAwait(false);
1✔
491
                    }
492
                    else if (this.InvalidPrompt != null)
1✔
493
                    {
494
                        template = this.InvalidPrompt;
1✔
495
                        msg = await this.InvalidPrompt.BindAsync(dc, cancellationToken: cancellationToken).ConfigureAwait(false);
1✔
496
                    }
497

498
                    break;
1✔
499

500
                case InputState.Invalid:
501
                    if (this.InvalidPrompt != null)
1✔
502
                    {
503
                        template = this.InvalidPrompt;
1✔
504
                        msg = await this.InvalidPrompt.BindAsync(dc, cancellationToken: cancellationToken).ConfigureAwait(false);
1✔
505
                    }
506
                    else if (this.UnrecognizedPrompt != null)
1✔
507
                    {
508
                        template = this.UnrecognizedPrompt;
1✔
509
                        msg = await this.UnrecognizedPrompt.BindAsync(dc, cancellationToken: cancellationToken).ConfigureAwait(false);
1✔
510
                    }
511

512
                    break;
513
            }
514

515
            if (msg == null)
1✔
516
            {
517
                template = this.Prompt ?? throw new InvalidOperationException($"InputDialog is missing Prompt.");
×
518
                msg = await this.Prompt.BindAsync(dc, cancellationToken: cancellationToken).ConfigureAwait(false);
1✔
519
            }
520

521
            if (msg != null && string.IsNullOrEmpty(msg.InputHint))
1✔
522
            {
523
                msg.InputHint = InputHints.ExpectingInput;
1✔
524
            }
525

526
            TrackGeneratorResultEvent(dc, template, msg);
1✔
527

528
            return msg;
1✔
529
        }
1✔
530

531
        private async Task<InputState> RecognizeInputAsync(DialogContext dc, int turnCount, CancellationToken cancellationToken = default(CancellationToken))
532
        {
533
            dynamic input = null;
1✔
534

535
            // Use Property expression for input first
536
            if (this.Property != null)
1✔
537
            {
538
                var property = this.Property.GetValue(dc.State);
1✔
539
                dc.State.TryGetValue(property, out input);
1✔
540

541
                // Clear property to avoid it being stuck on the next turn. It will get written 
542
                // back if the value passes validations.
543
                dc.State.SetValue(property, null);
1✔
544
            }
545

546
            // Use Value expression for input second
547
            if (input == null && this.Value != null)
1✔
548
            {
549
                var (value, valueError) = this.Value.TryGetValue(dc.State);
1✔
550
                if (valueError != null)
1✔
551
                {
552
                    throw new InvalidOperationException($"In InputDialog, this.Value expression evaluation resulted in an error. Expression: {this.Value}. Error: {valueError}");
×
553
                }
554

555
                input = value;
1✔
556
            }
557

558
            // Fallback to using activity
559
            bool activityProcessed = dc.State.GetBoolValue(TurnPath.ActivityProcessed);
1✔
560
            if (!activityProcessed && input == null && turnCount > 0)
1✔
561
            {
562
                if (typeof(AttachmentInput).IsAssignableFrom(this.GetType()))
1✔
563
                {
564
                    input = dc.Context.Activity.Attachments ?? new List<Attachment>();
1✔
565
                }
566
                else
567
                {
568
                    input = dc.Context.Activity.Text;
1✔
569

570
                    // if there is no visible text AND we have a value object, then fallback to that.
571
                    if (string.IsNullOrEmpty(dc.Context.Activity.Text) && dc.Context.Activity.Value != null)
×
572
                    {
573
                        input = dc.Context.Activity.Value;
×
574
                    }
575
                }
576
            }
577

578
            // Update "this.value" and perform additional recognition and validations
579
            dc.State.SetValue(VALUE_PROPERTY, input);
1✔
580
            if (input != null)
1✔
581
            {
582
                var state = await this.OnRecognizeInputAsync(dc, cancellationToken).ConfigureAwait(false);
1✔
583
                if (state == InputState.Valid)
1✔
584
                {
585
                    foreach (var validation in this.Validations)
1✔
586
                    {
587
                        var value = validation.GetValue(dc.State);
1✔
588
                        if (value == false)
1✔
589
                        {
590
                            return InputState.Invalid;
1✔
591
                        }
592
                    }
593

594
                    dc.State.SetValue(TurnPath.ActivityProcessed, true);
1✔
595
                    return InputState.Valid;
1✔
596
                }
597
                else
598
                {
599
                    return state;
1✔
600
                }
601
            }
602
            else
603
            {
604
                return InputState.Missing;
1✔
605
            }
606
        }
1✔
607

608
        private async Task<DialogTurnResult> PromptUserAsync(DialogContext dc, InputState state, CancellationToken cancellationToken = default(CancellationToken))
609
        {
610
            var prompt = await this.OnRenderPromptAsync(dc, state, cancellationToken).ConfigureAwait(false);
1✔
611

612
            if (prompt == null)
1✔
613
            {
614
                throw new InvalidOperationException($"Call to OnRenderPromptAsync() returned a null activity for state {state}.");
1✔
615
            }
616

617
            await dc.Context.SendActivityAsync(prompt, cancellationToken).ConfigureAwait(false);
1✔
618

619
            return Dialog.EndOfTurn;
1✔
620
        }
1✔
621
    }
622
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc