22// Licensed under the MIT License.
33
44using System ;
5- using System . Collections ;
65using System . Collections . Generic ;
76using System . Linq ;
87using System . Threading ;
@@ -17,31 +16,36 @@ namespace Microsoft.BotBuilderSamples
1716{
1817 /// <summary>
1918 /// Represents a bot that processes incoming activities.
20- /// For each user interaction, an instance of this class is created and the OnTurnAsync method is called.
21- /// This is a Transient lifetime service. Transient lifetime services are created
22- /// each time they're requested. For each Activity received, a new instance of this
23- /// class is created. Objects that are expensive to construct, or have a lifetime
24- /// beyond the single turn, should be carefully managed.
25- /// For example, the <see cref="MemoryStorage"/> object and associated
26- /// <see cref="IStatePropertyAccessor{T}"/> object are created with a singleton lifetime.
19+ /// For each user interaction, an instance of this class is created and the OnTurnAsync method
20+ /// is called. This is a Transient lifetime service. Transient lifetime services are created
21+ /// each time they're requested. For each Activity received, a new instance of this class is
22+ /// created. Objects that are expensive to construct, or have a lifetime beyond the single
23+ /// turn, should be carefully managed. For example, the <see cref="MemoryStorage"/> object and
24+ /// associated <see cref="IStatePropertyAccessor{T}"/> object are created with a singleton lifetime.
2725 /// </summary>
2826 public class DialogPromptBot : IBot
2927 {
3028 // Define identifiers for our dialogs and prompts.
3129 private const string ReservationDialog = "reservationDialog" ;
32- private const string PartySizePrompt = "partyPrompt " ;
30+ private const string SizeRangePrompt = "sizeRangePrompt " ;
3331 private const string LocationPrompt = "locationPrompt" ;
3432 private const string ReservationDatePrompt = "reservationDatePrompt" ;
3533
34+ // Define keys for tracked values within the dialog.
35+ private const string LocationKey = "location" ;
36+ private const string PartySizeKey = "partySize" ;
37+
3638 private readonly DialogSet _dialogSet ;
3739 private readonly DialogPromptBotAccessors _accessors ;
3840 private readonly ILogger _logger ;
3941
4042 /// <summary>
4143 /// Initializes a new instance of the <see cref="DialogPromptBot"/> class.
4244 /// </summary>
43- /// <param name="accessors">A class containing <see cref="IStatePropertyAccessor{T}"/> used to manage state.</param>
44- /// <param name="loggerFactory">A <see cref="ILoggerFactory"/> that is hooked to the Azure App Service provider.</param>
45+ /// <param name="accessors">A class containing <see cref="IStatePropertyAccessor{T}"/> used
46+ /// to manage state.</param>
47+ /// <param name="loggerFactory">A <see cref="ILoggerFactory"/> that is hooked to the Azure
48+ /// App Service provider.</param>
4549 public DialogPromptBot ( DialogPromptBotAccessors accessors , ILoggerFactory loggerFactory )
4650 {
4751 if ( loggerFactory == null )
@@ -55,18 +59,20 @@ public DialogPromptBot(DialogPromptBotAccessors accessors, ILoggerFactory logger
5559
5660 // Create the dialog set and add the prompts, including custom validation.
5761 _dialogSet = new DialogSet ( _accessors . DialogStateAccessor ) ;
58- _dialogSet . Add ( new NumberPrompt < int > ( PartySizePrompt , PartySizeValidatorAsync ) ) ;
62+
63+ _dialogSet . Add ( new NumberPrompt < int > ( SizeRangePrompt , RangeValidatorAsync ) ) ;
5964 _dialogSet . Add ( new ChoicePrompt ( LocationPrompt ) ) ;
6065 _dialogSet . Add ( new DateTimePrompt ( ReservationDatePrompt , DateValidatorAsync ) ) ;
6166
6267 // Define the steps of the waterfall dialog and add it to the set.
63- WaterfallStep [ ] steps = new WaterfallStep [ ]
68+ var steps = new WaterfallStep [ ]
6469 {
6570 PromptForPartySizeAsync ,
6671 PromptForLocationAsync ,
6772 PromptForReservationDateAsync ,
6873 AcknowledgeReservationAsync ,
6974 } ;
75+
7076 _dialogSet . Add ( new WaterfallDialog ( ReservationDialog , steps ) ) ;
7177 }
7278
@@ -77,25 +83,29 @@ public DialogPromptBot(DialogPromptBotAccessors accessors, ILoggerFactory logger
7783 /// </summary>
7884 /// <param name="turnContext">A <see cref="ITurnContext"/> containing all the data needed
7985 /// for processing this conversation turn. </param>
80- /// <param name="cancellationToken">(Optional) A <see cref="CancellationToken"/> that can be used by other objects
81- /// or threads to receive notice of cancellation.</param>
86+ /// <param name="cancellationToken">(Optional) A <see cref="CancellationToken"/> that can
87+ /// be used by other objects or threads to receive notice of cancellation.</param>
8288 /// <returns>A <see cref="Task"/> that represents the work queued to execute.</returns>
8389 /// <seealso cref="BotStateSet"/>
8490 /// <seealso cref="ConversationState"/>
8591 /// <seealso cref="IMiddleware"/>
86- public async Task OnTurnAsync ( ITurnContext turnContext , CancellationToken cancellationToken = default ( CancellationToken ) )
92+ public async Task OnTurnAsync (
93+ ITurnContext turnContext ,
94+ CancellationToken cancellationToken = default ( CancellationToken ) )
8795 {
8896 switch ( turnContext . Activity . Type )
8997 {
9098 // On a message from the user:
9199 case ActivityTypes . Message :
92100
93101 // Get the current reservation info from state.
94- Reservation reservation = await _accessors . ReservationAccessor . GetAsync (
95- turnContext , ( ) => null , cancellationToken ) ;
102+ var reservation = await _accessors . ReservationAccessor . GetAsync (
103+ turnContext ,
104+ ( ) => null ,
105+ cancellationToken ) ;
96106
97107 // Generate a dialog context for our dialog set.
98- DialogContext dc = await _dialogSet . CreateContextAsync ( turnContext , cancellationToken ) ;
108+ var dc = await _dialogSet . CreateContextAsync ( turnContext , cancellationToken ) ;
99109
100110 if ( dc . ActiveDialog is null )
101111 {
@@ -116,7 +126,7 @@ await turnContext.SendActivityAsync(
116126 else
117127 {
118128 // Continue the dialog.
119- DialogTurnResult dialogTurnResult = await dc . ContinueDialogAsync ( cancellationToken ) ;
129+ var dialogTurnResult = await dc . ContinueDialogAsync ( cancellationToken ) ;
120130
121131 // If the dialog completed this turn, record the reservation info.
122132 if ( dialogTurnResult . Status is DialogTurnStatus . Complete )
@@ -129,13 +139,15 @@ await _accessors.ReservationAccessor.SetAsync(
129139
130140 // Send a confirmation message to the user.
131141 await turnContext . SendActivityAsync (
132- $ "Your party of { reservation . Size } is confirmed for { reservation . Date } .",
142+ $ "Your party of { reservation . Size } is confirmed for " +
143+ $ "{ reservation . Date } in { reservation . Location } .",
133144 cancellationToken : cancellationToken ) ;
134145 }
135146 }
136147
137148 // Save the updated dialog state into the conversation state.
138- await _accessors . ConversationState . SaveChangesAsync ( turnContext , false , cancellationToken ) ;
149+ await _accessors . ConversationState . SaveChangesAsync (
150+ turnContext , false , cancellationToken ) ;
139151 break ;
140152 }
141153 }
@@ -152,23 +164,33 @@ private async Task<DialogTurnResult> PromptForPartySizeAsync(
152164 {
153165 // Prompt for the party size. The result of the prompt is returned to the next step of the waterfall.
154166 return await stepContext . PromptAsync (
155- PartySizePrompt ,
167+ SizeRangePrompt ,
156168 new PromptOptions
157169 {
158170 Prompt = MessageFactory . Text ( "How many people is the reservation for?" ) ,
159171 RetryPrompt = MessageFactory . Text ( "How large is your party?" ) ,
172+ Validations = new Range { Min = 3 , Max = 8 } ,
160173 } ,
161174 cancellationToken ) ;
162175 }
163176
164- private async Task < DialogTurnResult > PromptForLocationAsync ( WaterfallStepContext stepContext , CancellationToken cancellationToken )
177+ /// <summary>Second step of the main dialog: prompt for location.</summary>
178+ /// <param name="stepContext">The context for the waterfall step.</param>
179+ /// <param name="cancellationToken">A cancellation token that can be used by other objects
180+ /// or threads to receive notice of cancellation.</param>
181+ /// <returns>A task that represents the work queued to execute.</returns>
182+ /// <remarks>If the task is successful, the result contains information from this step.</remarks>
183+ private async Task < DialogTurnResult > PromptForLocationAsync (
184+ WaterfallStepContext stepContext ,
185+ CancellationToken cancellationToken )
165186 {
166187 // Record the party size information in the current dialog state.
167- int size = ( int ) stepContext . Result ;
168- stepContext . Values [ "size" ] = size ;
188+ var size = ( int ) stepContext . Result ;
189+ stepContext . Values [ PartySizeKey ] = size ;
169190
191+ // Prompt for the location.
170192 return await stepContext . PromptAsync (
171- "locationPrompt" ,
193+ LocationPrompt ,
172194 new PromptOptions
173195 {
174196 Prompt = MessageFactory . Text ( "Please choose a location." ) ,
@@ -178,8 +200,7 @@ private async Task<DialogTurnResult> PromptForLocationAsync(WaterfallStepContext
178200 cancellationToken ) ;
179201 }
180202
181- /// <summary>Second step of the main dialog: record the party size and prompt for the
182- /// reservation date.</summary>
203+ /// <summary>Third step of the main dialog: prompt for the reservation date.</summary>
183204 /// <param name="stepContext">The context for the waterfall step.</param>
184205 /// <param name="cancellationToken">A cancellation token that can be used by other objects
185206 /// or threads to receive notice of cancellation.</param>
@@ -190,10 +211,10 @@ private async Task<DialogTurnResult> PromptForReservationDateAsync(
190211 CancellationToken cancellationToken = default ( CancellationToken ) )
191212 {
192213 // Record the party size information in the current dialog state.
193- var location = stepContext . Result ;
194- stepContext . Values [ "location" ] = location ;
214+ var location = ( stepContext . Result as FoundChoice ) . Value ;
215+ stepContext . Values [ LocationKey ] = location ;
195216
196- // Prompt for the party size. The result of the prompt is returned to the next step of the waterfall .
217+ // Prompt for the reservation date .
197218 return await stepContext . PromptAsync (
198219 ReservationDatePrompt ,
199220 new PromptOptions
@@ -204,7 +225,7 @@ private async Task<DialogTurnResult> PromptForReservationDateAsync(
204225 cancellationToken ) ;
205226 }
206227
207- /// <summary>Third step of the main dialog: return the collected party size and reservation date .</summary>
228+ /// <summary>Last step of the main dialog: return the collected reservation information .</summary>
208229 /// <param name="stepContext">The context for the waterfall step.</param>
209230 /// <param name="cancellationToken">A cancellation token that can be used by other objects
210231 /// or threads to receive notice of cancellation.</param>
@@ -215,19 +236,20 @@ private async Task<DialogTurnResult> AcknowledgeReservationAsync(
215236 CancellationToken cancellationToken = default ( CancellationToken ) )
216237 {
217238 // Retrieve the reservation date.
218- DateTimeResolution resolution = ( stepContext . Result as IList < DateTimeResolution > ) . First ( ) ;
219- string time = resolution . Value ?? resolution . Start ;
239+ var resolution = ( stepContext . Result as IList < DateTimeResolution > ) . First ( ) ;
240+ var time = resolution . Value ?? resolution . Start ;
220241
221242 // Send an acknowledgement to the user.
222243 await stepContext . Context . SendActivityAsync (
223244 "Thank you. We will confirm your reservation shortly." ,
224245 cancellationToken : cancellationToken ) ;
225246
226247 // Return the collected information to the parent context.
227- Reservation reservation = new Reservation
248+ var reservation = new Reservation
228249 {
229250 Date = time ,
230- Size = ( int ) stepContext . Values [ "size" ] ,
251+ Size = ( int ) stepContext . Values [ PartySizeKey ] ,
252+ Location = ( string ) stepContext . Values [ LocationKey ] ,
231253 } ;
232254 return await stepContext . EndDialogAsync ( reservation , cancellationToken ) ;
233255 }
@@ -239,7 +261,7 @@ await stepContext.Context.SendActivityAsync(
239261 /// <returns>A task that represents the work queued to execute.</returns>
240262 /// <remarks>Reservations can be made for groups of 6 to 20 people.
241263 /// If the task is successful, the result indicates whether the input was valid.</remarks>
242- private async Task < bool > PartySizeValidatorAsync (
264+ private async Task < bool > RangeValidatorAsync (
243265 PromptValidatorContext < int > promptContext ,
244266 CancellationToken cancellationToken )
245267 {
@@ -253,11 +275,17 @@ await promptContext.Context.SendActivityAsync(
253275 }
254276
255277 // Check whether the party size is appropriate.
256- int size = promptContext . Recognized . Value ;
257- if ( size < 6 || size > 20 )
278+ var size = promptContext . Recognized . Value ;
279+ var validRange = promptContext . Options . Validations as Range ;
280+ if ( size < validRange . Min || size > validRange . Max )
258281 {
259- await promptContext . Context . SendActivityAsync (
260- "Sorry, we can only take reservations for parties of 6 to 20." ,
282+ await promptContext . Context . SendActivitiesAsync (
283+ new Activity [ ]
284+ {
285+ MessageFactory . Text ( $ "Sorry, we can only take reservations for parties " +
286+ $ "of { validRange . Min } to { validRange . Max } .") ,
287+ promptContext . Options . RetryPrompt ,
288+ } ,
261289 cancellationToken : cancellationToken ) ;
262290 return false ;
263291 }
@@ -287,9 +315,9 @@ await promptContext.Context.SendActivityAsync(
287315
288316 // Check whether any of the recognized date-times are appropriate,
289317 // and if so, return the first appropriate date-time.
290- DateTime earliest = DateTime . Now . AddHours ( 1.0 ) ;
291- DateTimeResolution value = promptContext . Recognized . Value . FirstOrDefault ( v =>
292- DateTime . TryParse ( v . Value ?? v . Start , out DateTime time ) && DateTime . Compare ( earliest , time ) <= 0 ) ;
318+ var earliest = DateTime . Now . AddHours ( 1.0 ) ;
319+ var value = promptContext . Recognized . Value . FirstOrDefault ( v =>
320+ DateTime . TryParse ( v . Value ?? v . Start , out var time ) && DateTime . Compare ( earliest , time ) <= 0 ) ;
293321 if ( value != null )
294322 {
295323 promptContext . Recognized . Value . Clear ( ) ;
@@ -303,10 +331,21 @@ await promptContext.Context.SendActivityAsync(
303331 return false ;
304332 }
305333
334+ /// <summary>Describes an acceptable range of values.</summary>
335+ public class Range
336+ {
337+ public int Min { get ; set ; }
338+
339+ public int Max { get ; set ; }
340+ }
341+
342+ /// <summary>Holds a user's reservation information.</summary>
306343 public class Reservation
307344 {
308345 public int Size { get ; set ; }
309346
347+ public string Location { get ; set ; }
348+
310349 public string Date { get ; set ; }
311350 }
312351 }
0 commit comments