FormBuilder.cs
1 //
2 // Copyright (c) Microsoft. All rights reserved.
3 // Licensed under the MIT license.
4 //
5 // Microsoft Bot Framework: http://botframework.com
6 //
7 // Bot Builder SDK GitHub:
8 // https://github.com/Microsoft/BotBuilder
9 //
10 // Copyright (c) Microsoft Corporation
11 // All rights reserved.
12 //
13 // MIT License:
14 // Permission is hereby granted, free of charge, to any person obtaining
15 // a copy of this software and associated documentation files (the
16 // "Software"), to deal in the Software without restriction, including
17 // without limitation the rights to use, copy, modify, merge, publish,
18 // distribute, sublicense, and/or sell copies of the Software, and to
19 // permit persons to whom the Software is furnished to do so, subject to
20 // the following conditions:
21 //
22 // The above copyright notice and this permission notice shall be
23 // included in all copies or substantial portions of the Software.
24 //
25 // THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND,
26 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
27 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
28 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
29 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
30 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
31 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
32 //
33 
36 using Microsoft.Bot.Connector;
37 using System;
38 using System.Collections;
39 using System.Collections.Generic;
40 using System.Globalization;
41 using System.Linq;
42 using System.Reflection;
43 using System.Resources;
44 using System.Threading;
46 using System.Threading.Tasks;
47 using System.Text;
48 
49 namespace Microsoft.Bot.Builder.FormFlow
50 {
51  #region Documentation
52  #endregion
55  public abstract class FormBuilderBase<T> : IFormBuilder<T>
56  where T : class
57  {
58  public virtual IForm<T> Build(Assembly resourceAssembly = null, string resourceName = null)
59  {
60  if (resourceAssembly == null)
61  {
62  resourceAssembly = typeof(T).Assembly;
63  }
64  if (resourceName == null)
65  {
66  resourceName = typeof(T).FullName;
67  }
68  if (this._form._prompter == null)
69  {
70  this._form._prompter = async (context, prompt, state, field) =>
71  {
72  var preamble = context.MakeMessage();
73  var promptMessage = context.MakeMessage();
74  if (prompt.GenerateMessages(preamble, promptMessage))
75  {
76  await context.PostAsync(preamble);
77  }
78  await context.PostAsync(promptMessage);
79  return prompt;
80  };
81  }
82  var lang = resourceAssembly.GetCustomAttribute<NeutralResourcesLanguageAttribute>();
83  if (lang != null && !string.IsNullOrWhiteSpace(lang.CultureName))
84  {
85  IEnumerable<string> missing, extra;
86  string name = null;
87  foreach (var resource in resourceAssembly.GetManifestResourceNames())
88  {
89  if (resource.Contains(resourceName))
90  {
91  var pieces = resource.Split('.');
92  name = string.Join(".", pieces.Take(pieces.Count() - 1));
93  break;
94  }
95  }
96  if (name != null)
97  {
98  var rm = new ResourceManager(name, resourceAssembly);
99  var rs = rm.GetResourceSet(Thread.CurrentThread.CurrentUICulture, true, true);
100  _form.Localize(rs.GetEnumerator(), out missing, out extra);
101  if (missing.Any())
102  {
103  throw new MissingManifestResourceException($"Missing resources {missing}");
104  }
105  }
106  }
107  Validate();
108  return this._form;
109  }
110 
111  public FormConfiguration Configuration { get { return _form.Configuration; } }
112 
113  public bool HasField(string name)
114  {
115  return _form.Fields.Field(name) != null;
116  }
117 
118  public virtual IFormBuilder<T> Message(string message, ActiveDelegate<T> condition = null, IEnumerable<string> dependencies = null)
119  {
120  _form._steps.Add(new MessageStep<T>(new PromptAttribute(message), condition, dependencies, _form));
121  return this;
122  }
123 
124  public virtual IFormBuilder<T> Message(PromptAttribute prompt, ActiveDelegate<T> condition = null, IEnumerable<string> dependencies = null)
125  {
126  _form._steps.Add(new MessageStep<T>(prompt, condition, dependencies, _form));
127  return this;
128  }
129 
130  public virtual IFormBuilder<T> Message(MessageDelegate<T> generateMessage, ActiveDelegate<T> condition = null, IEnumerable<string> dependencies = null)
131  {
132  _form._steps.Add(new MessageStep<T>(generateMessage, condition, dependencies, _form));
133  return this;
134  }
135 
136  public virtual IFormBuilder<T> Field(IField<T> field)
137  {
138  return AddField(field);
139  }
140 
141  public virtual IFormBuilder<T> Confirm(string prompt, ActiveDelegate<T> condition = null, IEnumerable<string> dependencies = null)
142  {
143  return Confirm(new PromptAttribute(prompt) { ChoiceFormat = Resources.ConfirmChoiceFormat, AllowDefault = BoolDefault.False }, condition, dependencies);
144  }
145 
146  public virtual IFormBuilder<T> Confirm(PromptAttribute prompt, ActiveDelegate<T> condition = null, IEnumerable<string> dependencies = null)
147  {
148  if (condition == null) condition = state => true;
149  dependencies = dependencies ?? _form.Dependencies(_form.Steps.Count());
150  var confirmation = new Confirmation<T>(prompt, condition, dependencies, _form);
151  confirmation.Form = _form;
152  _form._fields.Add(confirmation);
153  _form._steps.Add(new ConfirmStep<T>(confirmation));
154  return this;
155  }
156 
157  public virtual IFormBuilder<T> Confirm(MessageDelegate<T> generateMessage, ActiveDelegate<T> condition = null, IEnumerable<string> dependencies = null)
158  {
159  if (condition == null) condition = state => true;
160  dependencies = dependencies ?? _form.Dependencies(_form.Steps.Count());
161  var confirmation = new Confirmation<T>(generateMessage, condition, dependencies, _form);
162  confirmation.Form = _form;
163  _form._fields.Add(confirmation);
164  _form._steps.Add(new ConfirmStep<T>(confirmation));
165  return this;
166  }
167 
168  public virtual IFormBuilder<T> OnCompletion(OnCompletionAsyncDelegate<T> callback)
169  {
170  _form._completion = callback;
171  return this;
172  }
173 
174  public virtual IFormBuilder<T> Prompter(PromptAsyncDelegate<T> prompter)
175  {
176  _form._prompter = prompter;
177  return this;
178  }
179 
180  public abstract IFormBuilder<T> Field(string name, ActiveDelegate<T> active = null, ValidateAsyncDelegate<T> validate = null);
181  public abstract IFormBuilder<T> Field(string name, string prompt, ActiveDelegate<T> active = null, ValidateAsyncDelegate<T> validate = null);
182  public abstract IFormBuilder<T> Field(string name, PromptAttribute prompt, ActiveDelegate<T> active = null, ValidateAsyncDelegate<T> validate = null);
183  public abstract IFormBuilder<T> AddRemainingFields(IEnumerable<string> exclude = null);
184 
185  private Dictionary<TemplateUsage, int> _templateArgs = new Dictionary<TemplateUsage, int>
186  {
187  {TemplateUsage.Bool, 0 },
188  { TemplateUsage.BoolHelp, 1},
189  { TemplateUsage.Clarify, 1},
190  { TemplateUsage.Confirmation, 0 },
191  { TemplateUsage.CurrentChoice, 0},
192  { TemplateUsage.DateTime, 0},
193  { TemplateUsage.DateTimeHelp, 2},
194  { TemplateUsage.Double, 2},
195  { TemplateUsage.DoubleHelp, 4},
196  { TemplateUsage.EnumManyNumberHelp, 3},
197  { TemplateUsage.EnumOneNumberHelp, 3},
198  { TemplateUsage.EnumManyWordHelp, 3},
199  { TemplateUsage.EnumOneWordHelp, 3},
200  { TemplateUsage.EnumSelectOne, 0},
201  { TemplateUsage.EnumSelectMany, 0},
202  { TemplateUsage.Feedback, 1},
203  { TemplateUsage.Help, 2},
204  { TemplateUsage.HelpClarify, 2},
205  { TemplateUsage.HelpConfirm, 2},
206  { TemplateUsage.HelpNavigation, 2},
207  { TemplateUsage.Integer, 2},
208  { TemplateUsage.IntegerHelp, 4},
209  { TemplateUsage.Navigation, 0},
210  { TemplateUsage.NavigationCommandHelp, 1},
211  { TemplateUsage.NavigationFormat, 0},
212  { TemplateUsage.NavigationHelp, 2},
213  { TemplateUsage.NoPreference, 0},
214  { TemplateUsage.NotUnderstood, 1},
215  { TemplateUsage.StatusFormat, 0},
216  { TemplateUsage.String, 0},
217  { TemplateUsage.StringHelp, 2},
218  { TemplateUsage.Unspecified, 0},
219  };
220 
221  private int TemplateArgs(TemplateUsage usage)
222  {
223  int args;
224  if (!_templateArgs.TryGetValue(usage, out args))
225  {
226  throw new ArgumentException("Missing template usage for validation");
227  }
228  return args;
229  }
230 
231  private void Validate()
232  {
233  foreach (var step in _form._steps)
234  {
235  // Validate prompt
236  var annotation = step.Annotation;
237  if (annotation != null)
238  {
239  var name = step.Type == StepType.Field ? step.Name : "";
240  foreach (var pattern in annotation.Patterns)
241  {
242  ValidatePattern(pattern, _form.Fields.Field(name), 5);
243  }
244  if (step.Type != StepType.Message)
245  {
246  foreach (TemplateUsage usage in Enum.GetValues(typeof(TemplateUsage)))
247  {
248  if (usage != TemplateUsage.None)
249  {
250  foreach (var pattern in step.Field.Template(usage).Patterns)
251  {
252  ValidatePattern(pattern, _form.Fields.Field(name), TemplateArgs(usage));
253  }
254  }
255  }
256  }
257  }
258  }
259  ValidatePattern(_form.Configuration.DefaultPrompt.ChoiceFormat, null, 2);
260  }
261 
262  private void ValidatePattern(string pattern, IField<T> field, int maxArgs)
263  {
264  if (!Prompter<T>.ValidatePattern(_form, pattern, field, maxArgs))
265  {
266  throw new ArgumentException(string.Format("Illegal pattern: \"{0}\"", pattern));
267  }
268  }
269 
270  private IFormBuilder<T> AddField(IField<T> field)
271  {
272  field.Form = _form;
273  _form._fields.Add(field);
274  var step = new FieldStep<T>(field.Name, _form);
275  var stepIndex = this._form._steps.FindIndex(s => s.Name == field.Name);
276  if (stepIndex >= 0)
277  {
278  _form._steps[stepIndex] = step;
279  }
280  else
281  {
282  _form._steps.Add(step);
283  }
284  return this;
285  }
286 
287  protected internal sealed class Form : IForm<T>
288  {
289  internal readonly FormConfiguration _configuration = new FormConfiguration();
290  internal readonly Fields<T> _fields = new Fields<T>();
291  internal readonly List<IStep<T>> _steps = new List<IStep<T>>();
292  internal OnCompletionAsyncDelegate<T> _completion = null;
293  internal PromptAsyncDelegate<T> _prompter = null;
294  internal ILocalizer _resources = new Localizer() { Culture = CultureInfo.CurrentUICulture };
295 
296  public Form()
297  {
298  }
299 
300  internal override ILocalizer Resources
301  {
302  get
303  {
304  return _resources;
305  }
306  }
307 
308  public override void SaveResources(IResourceWriter writer)
309  {
310  _resources = new Localizer() { Culture = CultureInfo.CurrentUICulture };
311  foreach (var step in _steps)
312  {
313  step.SaveResources();
314  }
315  _resources.Save(writer);
316  }
317 
318  public override void Localize(IDictionaryEnumerator reader, out IEnumerable<string> missing, out IEnumerable<string> extra)
319  {
320  foreach (var step in _steps)
321  {
322  step.SaveResources();
323  }
324  _resources = _resources.Load(reader, out missing, out extra);
325  foreach (var step in _steps)
326  {
327  step.Localize();
328  }
329  }
330 
331  internal override FormConfiguration Configuration
332  {
333  get
334  {
335  return _configuration;
336  }
337  }
338 
339  internal override IReadOnlyList<IStep<T>> Steps
340  {
341  get
342  {
343  return _steps;
344  }
345  }
346 
347  internal override async Task<FormPrompt> Prompt(IDialogContext context, FormPrompt prompt, T state, IField<T> field)
348  {
349  return prompt == null ? prompt : await _prompter(context, prompt, state, field);
350  }
351 
352  internal override OnCompletionAsyncDelegate<T> Completion
353  {
354  get
355  {
356  return _completion;
357  }
358  }
359 
360  public override IFields<T> Fields
361  {
362  get
363  {
364  return _fields;
365  }
366  }
367  }
368 
369  protected internal Form _form = new Form();
370  }
371 
372  #region Documentation
373  #endregion
387  public sealed class FormBuilder<T> : FormBuilderBase<T>
388  where T : class
389  {
390  private readonly bool _ignoreAnnotations;
391 
396  public FormBuilder(bool ignoreAnnotations = false)
397  : base()
398  {
399  _ignoreAnnotations = ignoreAnnotations;
400  }
401 
402  public override IForm<T> Build(Assembly resourceAssembly = null, string resourceName = null)
403  {
404  if (!_form._steps.Any((step) => step.Type == StepType.Field))
405  {
406  var paths = new List<string>();
407  FieldPaths(typeof(T), "", paths);
408  foreach (var path in paths)
409  {
410  Field(new FieldReflector<T>(path, _ignoreAnnotations));
411  }
412  Confirm(new PromptAttribute(_form.Configuration.Template(TemplateUsage.Confirmation)));
413  }
414  return base.Build(resourceAssembly, resourceName);
415  }
416 
417  public override IFormBuilder<T> Field(string name, ActiveDelegate<T> active = null, ValidateAsyncDelegate<T> validate = null)
418  {
419  var field = new FieldReflector<T>(name, _ignoreAnnotations);
420  field.SetActive(active);
421  field.SetValidate(validate);
422  return Field(field);
423  }
424 
425  public override IFormBuilder<T> Field(string name, string prompt, ActiveDelegate<T> active = null, ValidateAsyncDelegate<T> validate = null)
426  {
427  return Field(name, new PromptAttribute(prompt), active, validate);
428  }
429 
430  public override IFormBuilder<T> Field(string name, PromptAttribute prompt, ActiveDelegate<T> active = null, ValidateAsyncDelegate<T> validate = null)
431  {
432  var field = new FieldReflector<T>(name, _ignoreAnnotations);
433  field.SetActive(active);
434  field.SetValidate(validate);
435  field.SetPrompt(prompt);
436  return Field(field);
437  }
438 
439  public override IFormBuilder<T> AddRemainingFields(IEnumerable<string> exclude = null)
440  {
441  var exclusions = (exclude == null ? Array.Empty<string>() : exclude.ToArray());
442  var paths = new List<string>();
443  FieldPaths(typeof(T), "", paths);
444  foreach (var path in paths)
445  {
446  if (!exclusions.Contains(path) && !HasField(path))
447  {
448  Field(new FieldReflector<T>(path, _ignoreAnnotations));
449  }
450  }
451  return this;
452  }
453 
454  private void FieldPaths(Type type, string path, List<string> paths)
455  {
456  var newPath = (path == "" ? path : path + ".");
457  foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance))
458  {
459  TypePaths(field.FieldType, newPath + field.Name, paths);
460  }
461 
462  foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
463  {
464  if (property.CanRead && property.CanWrite)
465  {
466  TypePaths(property.PropertyType, newPath + property.Name, paths);
467  }
468  }
469  }
470 
471  private void TypePaths(Type type, string path, List<string> paths)
472  {
473  if (type.IsClass)
474  {
475  if (type == typeof(string))
476  {
477  paths.Add(path);
478  }
479  else if (type.IsIEnumerable())
480  {
481  var elt = type.GetGenericElementType();
482  if (elt.IsEnum)
483  {
484  paths.Add(path);
485  }
486  else
487  {
488  // TODO: What to do about enumerations of things other than enums?
489  }
490  }
491  else
492  {
493  FieldPaths(type, path, paths);
494  }
495  }
496  else if (type.IsEnum)
497  {
498  paths.Add(path);
499  }
500  else if (type == typeof(bool))
501  {
502  paths.Add(path);
503  }
504  else if (type.IsIntegral())
505  {
506  paths.Add(path);
507  }
508  else if (type.IsDouble())
509  {
510  paths.Add(path);
511  }
512  else if (type.IsNullable() && type.IsValueType)
513  {
514  paths.Add(path);
515  }
516  else if (type == typeof(DateTime))
517  {
518  paths.Add(path);
519  }
520  }
521  }
522 }
Core namespace for FormFlow and associated infrastructure.
Definition: Attributes.cs:39
The context for the execution of a dialog&#39;s conversational process.
virtual IFormBuilder< T > OnCompletion(OnCompletionAsyncDelegate< T > callback)
Delegate to call when form is completed.
Definition: FormBuilder.cs:168
A strongly-typed resource class, for looking up localized strings, etc.
Dictionary of all fields indexed by name.
Definition: Field.cs:785
virtual IFormBuilder< T > Message(string message, ActiveDelegate< T > condition=null, IEnumerable< string > dependencies=null)
Show a message that does not require a response.
Definition: FormBuilder.cs:118
Namespace for the Microsoft Bot Connector SDK.
virtual IFormBuilder< T > Prompter(PromptAsyncDelegate< T > prompter)
Delegate to send prompt to user.
Definition: FormBuilder.cs:174
A prompt and recognizer packaged together.
Definition: IPrompt.cs:330
override IFormBuilder< T > Field(string name, ActiveDelegate< T > active=null, ValidateAsyncDelegate< T > validate=null)
Define a step for filling in a particular value in the form state.
Definition: FormBuilder.cs:417
Define the prompt used when asking about a field.
Definition: Attributes.cs:294
Build a form by specifying messages, fields and confirmations via reflection or programatically.
Definition: FormBuilder.cs:387
TemplateUsage
All of the built-in templates.
Definition: Attributes.cs:321
Abstract base class for Form Builders.
Definition: FormBuilder.cs:55
Interface to track all of the fields in a form.
Definition: IField.cs:427
Interface for all the information about a specific field.
Definition: IField.cs:404
override IFormBuilder< T > Field(string name, PromptAttribute prompt, ActiveDelegate< T > active=null, ValidateAsyncDelegate< T > validate=null)
Define a step for filling in a particular value in the form state.
Definition: FormBuilder.cs:430
virtual IFormBuilder< T > Message(PromptAttribute prompt, ActiveDelegate< T > condition=null, IEnumerable< string > dependencies=null)
Show a message with more format control that does not require a response.
Definition: FormBuilder.cs:124
virtual IFormBuilder< T > Message(MessageDelegate< T > generateMessage, ActiveDelegate< T > condition=null, IEnumerable< string > dependencies=null)
Generate a message using a delegate to dynamically build the message.
Definition: FormBuilder.cs:130
virtual IForm< T > Build(Assembly resourceAssembly=null, string resourceName=null)
Build the form based on the methods called on the builder.
Definition: FormBuilder.cs:58
virtual IFormBuilder< T > Confirm(string prompt, ActiveDelegate< T > condition=null, IEnumerable< string > dependencies=null)
Add a confirmation step.
Definition: FormBuilder.cs:141
BoolDefault
Three state boolean value.
Definition: Attributes.cs:240
Interface for building a form.
Definition: IFormBuilder.cs:77
ILocalizer Load(IDictionaryEnumerator reader, out IEnumerable< string > missing, out IEnumerable< string > extra)
Load the localizer from a stream.
Namespace for FormFlow advanced building blocks.
Definition: Attributes.cs:672
bool HasField(string name)
Test to see if there is already a field with name .
Definition: FormBuilder.cs:113
Namespace for resources.
Form definition interface.
Definition: IForm.cs:47
void Save(IResourceWriter writer)
Save localizer resources to stream.
Field< T > SetActive(ActiveDelegate< T > condition)
Define a delegate for checking state to see if field applies.
Definition: Field.cs:524
Base class with declarative implementation of IField.
Definition: Field.cs:67
virtual IFormBuilder< T > Field(IField< T > field)
Derfine a field step by supplying your own field definition.
Definition: FormBuilder.cs:136
FormBuilder(bool ignoreAnnotations=false)
Create a new form builder for building a form using reflection.
Definition: FormBuilder.cs:396
Interface for localizing string resources.
Definition: ILocalizer.cs:44
string Name
Name of this field.
Definition: IField.cs:415
override IFormBuilder< T > AddRemainingFields(IEnumerable< string > exclude=null)
Add all fields not already added to the form.
Definition: FormBuilder.cs:439
IForm< T > Form
Form that owns this field
Definition: IField.cs:420
override IFormBuilder< T > Field(string name, string prompt, ActiveDelegate< T > active=null, ValidateAsyncDelegate< T > validate=null)
Define a step for filling in a particular value in the form state.
Definition: FormBuilder.cs:425
virtual IFormBuilder< T > Confirm(PromptAttribute prompt, ActiveDelegate< T > condition=null, IEnumerable< string > dependencies=null)
Add a confirmation step.
Definition: FormBuilder.cs:146
virtual IFormBuilder< T > Confirm(MessageDelegate< T > generateMessage, ActiveDelegate< T > condition=null, IEnumerable< string > dependencies=null)
Generate a confirmation using a delegate to dynamically build the message.
Definition: FormBuilder.cs:157
static string ConfirmChoiceFormat
Looks up a localized string similar to {1}.
Fill in field information through reflection.
Field is used to confirm some settings during the dialog.
Core namespace for Dialogs and associated infrastructure.
override IForm< T > Build(Assembly resourceAssembly=null, string resourceName=null)
Build the form based on the methods called on the builder.
Definition: FormBuilder.cs:402
Root namespace for the Microsoft Bot Builder SDK.
The prompt that is returned by form prompter.
Definition: IPrompt.cs:85