IPrompt.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.Generic;
39 using System.IO;
40 using System.Linq;
41 using System.Runtime.Serialization.Formatters.Binary;
42 using System.Text;
43 using System.Text.RegularExpressions;
44 using System.Threading.Tasks;
45 
46 namespace Microsoft.Bot.Builder.FormFlow.Advanced
47 {
48 
56  public interface IPrompt<T>
57  where T : class
58  {
63  TemplateBaseAttribute Annotation { get; }
64 
72  FormPrompt Prompt(T state, IField<T> field, params object[] args);
73 
78  IRecognize<T> Recognizer { get; }
79  }
80 
84  [Serializable]
85  public sealed class FormPrompt : ICloneable
86  {
91  public string Prompt { set; get; } = string.Empty;
92 
96  public DescribeAttribute Description { set; get; }
97 
101  public IList<DescribeAttribute> Buttons { set; get; } = new List<DescribeAttribute>();
102 
106  public ChoiceStyleOptions Style;
107 
108  public override string ToString()
109  {
110  return $"{Prompt} {Language.BuildList(Buttons.Select(button => button.ToString()), Resources.DefaultChoiceSeparator, Resources.DefaultChoiceLastSeparator)}";
111  }
112 
117  public object Clone()
118  {
119  var newPrompt = new FormPrompt();
120  newPrompt.Prompt = this.Prompt;
121  newPrompt.Description = this.Description;
122  newPrompt.Buttons = new List<DescribeAttribute>(this.Buttons);
123  newPrompt.Style = this.Style;
124  return newPrompt;
125  }
126  }
127 
131  [Serializable]
132  public sealed class FormButton : ICloneable
133  {
137  public string Image { get; set; }
138 
142  public string Message { get; set; }
143 
147  public string Title { get; set; }
148 
152  public string Url { get; set; }
153 
158  public object Clone()
159  {
160  return new FormButton
161  {
162  Image = this.Image,
163  Message = this.Message,
164  Title = this.Title,
165  Url = this.Url
166  };
167  }
168 
173  public override string ToString()
174  {
175  return Title;
176  }
177  }
178 
187  public delegate Task<FormPrompt> PromptAsyncDelegate<T>(IDialogContext context, FormPrompt prompt, T state, IField<T> field)
188  where T : class;
189 
190  public static partial class Extensions
191  {
197  public static IList<Attachment> GenerateHeroCard(this FormPrompt prompt)
198  {
199  var actions = new List<CardAction>();
200  foreach (var button in prompt.Buttons)
201  {
202  actions.Add(new CardAction(ActionTypes.ImBack, button.Description, button.Image, button.Message ?? button.Description));
203  }
204 
205  var attachments = new List<Attachment>();
206  if (actions.Count > 0)
207  {
208  var description = prompt.Description;
209  // Facebook requires a title https://github.com/Microsoft/BotBuilder/issues/1678
210  attachments.Add(new HeroCard(text: prompt.Prompt, title: description.Title ?? string.Empty, subtitle: description.SubTitle,
211  buttons: actions,
212  images: prompt.Description?.Image == null ? null : new List<CardImage>() { new CardImage() { Url = description.Image } })
213  .ToAttachment());
214  }
215  return attachments;
216  }
217 
223  public static IList<Attachment> GenerateHeroCards(this FormPrompt prompt)
224  {
225  var attachments = new List<Attachment>();
226  var description = prompt.Description;
227  foreach (var button in prompt.Buttons)
228  {
229  string image = button.Image ?? description.Image;
230  attachments.Add(new HeroCard(
231  title: button.Title ?? description.Title ?? string.Empty,
232  subtitle: button.SubTitle ?? description.SubTitle,
233  text: prompt.Prompt,
234  images: (image == null ? null : (new List<CardImage>() { new CardImage() { Url = image } })),
235  buttons: new List<CardAction>() { new CardAction(ActionTypes.ImBack, button.Description, null, button.Message ?? button.Description) })
236  .ToAttachment());
237  }
238  return attachments;
239  }
240 
248  public static bool GenerateMessages(this FormPrompt prompt, IMessageActivity preamble, IMessageActivity promptMessage)
249  {
250  var promptCopy = (FormPrompt) prompt.Clone();
251  bool hasPreamble = false;
252  if (promptCopy.Buttons?.Count > 0 || promptCopy.Description?.Image != null)
253  {
254  // If we are generating cards we do not support markdown so create a separate message
255  // for all lines except the last one.
256  var lines = promptCopy.Prompt.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);
257  if (lines.Length > 1)
258  {
259  var builder = new StringBuilder();
260  for (var i = 0; i < lines.Length - 1; ++i)
261  {
262  if (i > 0)
263  {
264  builder.AppendLine();
265  }
266  builder.Append(lines[i]);
267  }
268  preamble.Text = builder.ToString();
269  promptCopy.Prompt = lines.Last();
270  hasPreamble = true;
271  }
272  if (promptCopy.Buttons?.Count > 0)
273  {
274  var style = promptCopy.Style;
275  if (style == ChoiceStyleOptions.Auto)
276  {
277  foreach (var button in promptCopy.Buttons)
278  {
279  // Images require carousel
280  if (button.Image != null)
281  {
282  style = ChoiceStyleOptions.Carousel;
283  break;
284  }
285  }
286  }
287  if (style == ChoiceStyleOptions.Carousel)
288  {
290  promptMessage.Attachments = promptCopy.GenerateHeroCards();
291  }
292  else
293  {
295  promptMessage.Attachments = promptCopy.GenerateHeroCard();
296  }
297  }
298  else if (promptCopy.Description?.Image != null)
299  {
301  var card = new HeroCard() { Title = promptCopy.Prompt, Images = new List<CardImage> { new CardImage(promptCopy.Description.Image) } };
302  promptMessage.Attachments = new List<Attachment> { card.ToAttachment() };
303  }
304  }
305  else
306  {
307  promptMessage.Text = promptCopy.Prompt;
308  }
309  return hasPreamble;
310  }
311 
312  internal static void AddRange<T>(this ICollection<T> collection, IEnumerable<T> enumerable)
313  {
314  foreach (var cur in enumerable)
315  {
316  collection.Add(cur);
317  }
318  }
319 
320  internal static IList<T> Clone<T>(this IList<T> listToClone) where T : ICloneable
321  {
322  return listToClone.Select(item => (T)item.Clone()).ToList();
323  }
324  }
325 
326  #region Documentation
327  #endregion
330  public sealed class Prompter<T> : IPrompt<T>
331  where T : class
332  {
340  public Prompter(TemplateBaseAttribute annotation, IForm<T> form, IRecognize<T> recognizer, IFields<T> fields = null)
341  {
342  annotation.ApplyDefaults(form.Configuration.DefaultPrompt);
343  _annotation = annotation;
344  _form = form;
345  _fields = fields ?? form.Fields;
346  _recognizer = recognizer;
347  }
348 
349  public TemplateBaseAttribute Annotation
350  {
351  get
352  {
353  return _annotation;
354  }
355  }
356 
357  public FormPrompt Prompt(T state, IField<T> field, params object[] args)
358  {
359  string currentChoice = null;
360  string noValue = null;
361  if (field != null)
362  {
363  currentChoice = field.Template(TemplateUsage.CurrentChoice).Pattern();
364  if (field.Optional)
365  {
366  noValue = field.Template(TemplateUsage.NoPreference).Pattern();
367  }
368  else
369  {
370  noValue = field.Template(TemplateUsage.Unspecified).Pattern();
371  }
372  }
373  IList<DescribeAttribute> buttons = new List<DescribeAttribute>();
374  var response = ExpandTemplate(_annotation.Pattern(), currentChoice, noValue, state, field, args, ref buttons);
375  return new FormPrompt
376  {
377  Prompt = (response == null ? string.Empty : _spacesPunc.Replace(_spaces.Replace(Language.ANormalization(response), "$1 "), "$1")),
378  Description = field?.FieldDescription,
379  Buttons = buttons,
380  Style = _annotation.ChoiceStyle
381  };
382  }
383 
384  public IRecognize<T> Recognizer
385  {
386  get { return _recognizer; }
387  }
388 
389  #region Documentation
390  #endregion
397  public static bool ValidatePattern(IForm<T> form, string pattern, IField<T> field, int argLimit = 0)
398  {
399  bool ok = true;
400  var fields = form.Fields;
401  foreach (Match match in _args.Matches(pattern))
402  {
403  var expr = match.Groups[1].Value.Trim();
404  int numeric;
405  if (expr == "||")
406  {
407  ok = true;
408  }
409  else if (expr.StartsWith("&"))
410  {
411  var name = expr.Substring(1);
412  if (name == string.Empty && field != null) name = field.Name;
413  ok = (name == string.Empty || fields.Field(name) != null);
414  }
415  else if (expr.StartsWith("?"))
416  {
417  ok = ValidatePattern(form, expr.Substring(1), field, argLimit);
418  }
419  else if (expr.StartsWith("["))
420  {
421  if (expr.EndsWith("]"))
422  {
423  ok = ValidatePattern(form, expr.Substring(1, expr.Length - 2), field, argLimit);
424  }
425  else
426  {
427  ok = false;
428  }
429  }
430  else if (expr.StartsWith("*"))
431  {
432  ok = (expr == "*" || expr == "*filled");
433  }
434  else if (TryParseFormat(expr, out numeric))
435  {
436  ok = numeric <= argLimit - 1;
437  }
438  else
439  {
440  var formatArgs = expr.Split(':');
441  var name = formatArgs[0];
442  if (name == string.Empty && field != null) name = field.Name;
443  ok = (name == string.Empty || fields.Field(name) != null);
444  }
445  if (!ok)
446  {
447  break;
448  }
449  }
450  return ok;
451  }
452 
453  private string ExpandTemplate(string template, string currentChoice, string noValue, T state, IField<T> field, object[] args, ref IList<DescribeAttribute> buttons)
454  {
455  bool foundUnspecified = false;
456  int last = 0;
457  int numeric;
458  var response = new StringBuilder();
459 
460  foreach (Match match in _args.Matches(template))
461  {
462  var expr = match.Groups[1].Value.Trim();
463  var substitute = string.Empty;
464  if (expr.StartsWith("&"))
465  {
466  var name = expr.Substring(1);
467  if (name == string.Empty && field != null) name = field.Name;
468  var pathField = _fields.Field(name);
469  substitute = Language.Normalize(pathField == null ? field.Name : pathField.FieldDescription.Description, _annotation.FieldCase);
470  }
471  else if (expr == "||")
472  {
473  var builder = new StringBuilder();
474  var values = _recognizer.ValueDescriptions();
475  var useButtons = !field.AllowsMultiple
476  && (_annotation.ChoiceStyle == ChoiceStyleOptions.Auto
477  || _annotation.ChoiceStyle == ChoiceStyleOptions.Buttons
478  || _annotation.ChoiceStyle == ChoiceStyleOptions.Carousel);
479  if (values.Any() && _annotation.AllowDefault != BoolDefault.False && field.Optional)
480  {
481  values = values.Concat(new DescribeAttribute[] { new DescribeAttribute(Language.Normalize(noValue, _annotation.ChoiceCase)) });
482  }
483  string current = null;
484  if (_annotation.AllowDefault != BoolDefault.False)
485  {
486  if (!field.Optional)
487  {
488  if (!field.IsUnknown(state))
489  {
490  current = ExpandTemplate(currentChoice, null, noValue, state, field, args, ref buttons);
491  }
492  }
493  else
494  {
495  current = ExpandTemplate(currentChoice, null, noValue, state, field, args, ref buttons);
496  }
497  }
498  if (values.Any())
499  {
500  if (useButtons)
501  {
502  foreach (var value in values)
503  {
504  buttons.Add(value);
505  }
506  }
507  else
508  {
509  // Buttons do not support multiple selection so we fall back to text
510  if (((_annotation.ChoiceStyle == ChoiceStyleOptions.Auto || _annotation.ChoiceStyle == ChoiceStyleOptions.AutoText)
511  && values.Count() < 4)
512  || (_annotation.ChoiceStyle == ChoiceStyleOptions.Inline))
513  {
514  // Inline choices
515  if (_annotation.ChoiceParens == BoolDefault.True) builder.Append('(');
516  var choices = new List<string>();
517  var i = 1;
518  foreach (var value in values)
519  {
520  choices.Add(string.Format(_annotation.ChoiceFormat, i, Language.Normalize(value.Description, _annotation.ChoiceCase)));
521  ++i;
522  }
523  builder.Append(Language.BuildList(choices, _annotation.ChoiceSeparator, _annotation.ChoiceLastSeparator));
524  if (_annotation.ChoiceParens == BoolDefault.True) builder.Append(')');
525  if (current != null)
526  {
527  builder.Append(" ");
528  builder.Append(current);
529  }
530  }
531  else
532  {
533  // Separate line choices
534  if (current != null)
535  {
536  builder.Append(current);
537  builder.Append(" ");
538  }
539  var i = 1;
540  foreach (var value in values)
541  {
542  builder.AppendLine();
543  builder.Append(" ");
544  if (!_annotation.AllowNumbers)
545  {
546  builder.Append("* ");
547  }
548  builder.AppendFormat(_annotation.ChoiceFormat, i, Language.Normalize(value.Description, _annotation.ChoiceCase));
549  ++i;
550  }
551  }
552  }
553  }
554  else if (current != null)
555  {
556  builder.Append(" ");
557  builder.Append(current);
558  }
559  substitute = builder.ToString();
560  }
561  else if (expr.StartsWith("*"))
562  {
563  // Status display of active results
564  var filled = expr.ToLower().Trim().EndsWith("filled");
565  var builder = new StringBuilder();
566  if (match.Index > 0)
567  {
568  builder.AppendLine();
569  }
570  foreach (var entry in (from step in _fields where (!filled || !step.IsUnknown(state)) && step.Role == FieldRole.Value && step.Active(state) select step))
571  {
572  var format = new Prompter<T>(Template(entry, TemplateUsage.StatusFormat), _form, null);
573  builder.Append("* ").AppendLine(format.Prompt(state, entry).Prompt);
574  }
575  substitute = builder.ToString();
576  }
577  else if (expr.StartsWith("[") && expr.EndsWith("]"))
578  {
579  // Generate a list from multiple fields
580  var paths = expr.Substring(1, expr.Length - 2).Split(' ');
581  var values = new List<Tuple<IField<T>, object, string>>();
582  foreach (var spec in paths)
583  {
584  if (!spec.StartsWith("{") || !spec.EndsWith("}"))
585  {
586  throw new ArgumentException("Only {<field>} references are allowed in lists.");
587  }
588  var formatArgs = spec.Substring(1, spec.Length - 2).Trim().Split(':');
589  var name = formatArgs[0];
590  if (name == string.Empty && field != null) name = field.Name;
591  var format = (formatArgs.Length > 1 ? "0:" + formatArgs[1] : "0");
592  var eltDesc = _fields.Field(name);
593  if (!eltDesc.IsUnknown(state))
594  {
595  var value = eltDesc.GetValue(state);
596  if (value.GetType() != typeof(string) && value.GetType().IsIEnumerable())
597  {
598  var eltValues = (value as System.Collections.IEnumerable);
599  foreach (var elt in eltValues)
600  {
601  values.Add(Tuple.Create(eltDesc, elt, format));
602  }
603  }
604  else
605  {
606  values.Add(Tuple.Create(eltDesc, eltDesc.GetValue(state), format));
607  }
608  }
609  }
610  if (values.Count() > 0)
611  {
612  var elements = (from elt in values
613  select Language.Normalize(ValueDescription(elt.Item1, elt.Item2, elt.Item3), _annotation.ValueCase)).ToArray();
614  substitute = Language.BuildList(elements, _annotation.Separator, _annotation.LastSeparator);
615  }
616  }
617  else if (expr.StartsWith("?"))
618  {
619  // Conditional template
620  var subValue = ExpandTemplate(expr.Substring(1), currentChoice, null, state, field, args, ref buttons);
621  if (subValue == null)
622  {
623  substitute = string.Empty;
624  }
625  else
626  {
627  substitute = subValue;
628  }
629  }
630  else if (TryParseFormat(expr, out numeric))
631  {
632  // Process ad hoc arg
633  if (numeric < args.Length && args[numeric] != null)
634  {
635  substitute = string.Format("{" + expr + "}", args);
636  }
637  else
638  {
639  foundUnspecified = true;
640  break;
641  }
642  }
643  else
644  {
645  var formatArgs = expr.Split(':');
646  var name = formatArgs[0];
647  if (name == string.Empty && field != null) name = field.Name;
648  var pathDesc = _fields.Field(name);
649  if (pathDesc.IsUnknown(state))
650  {
651  if (noValue == null)
652  {
653  foundUnspecified = true;
654  break;
655  }
656  substitute = noValue;
657  }
658  else
659  {
660  var value = pathDesc.GetValue(state);
661  if (value.GetType() != typeof(string) && value.GetType().IsIEnumerable())
662  {
663  var values = (value as System.Collections.IEnumerable);
664  substitute = Language.BuildList(from elt in values.Cast<object>()
665  select Language.Normalize(ValueDescription(pathDesc, elt, "0"), _annotation.ValueCase),
666  _annotation.Separator, _annotation.LastSeparator);
667  }
668  else
669  {
670  var format = (formatArgs.Length > 1 ? "0:" + formatArgs[1] : "0");
671  substitute = ValueDescription(pathDesc, value, format);
672  }
673  }
674  }
675  response.Append(template.Substring(last, match.Index - last)).Append(substitute);
676  last = match.Index + match.Length;
677  }
678  return (foundUnspecified ? null : response.Append(template.Substring(last, template.Length - last)).ToString());
679  }
680 
681  private static bool TryParseFormat(string format, out int number)
682  {
683  var args = format.Split(':');
684  return int.TryParse(args[0], out number);
685  }
686 
687  private string ValueDescription(IField<T> field, object value, string format)
688  {
689  string result;
690  if (format != "0")
691  {
692  result = string.Format("{" + format + "}", value);
693  }
694  else
695  {
696  result = field.Prompt.Recognizer.ValueDescription(value).Description;
697  }
698  return result;
699  }
700 
701  private TemplateAttribute Template(IField<T> field, TemplateUsage usage)
702  {
703  return field == null
704  ? _form.Configuration.Template(usage)
705  : field.Template(usage);
706  }
707 
708  private static readonly Regex _args = new Regex(@"{((?>[^{}]+|{(?<number>)|}(?<-number>))*(?(number)(?!)))}", RegexOptions.Compiled);
709  private static readonly Regex _spaces = new Regex(@"(\S)( {2,})", RegexOptions.Compiled);
710  private static readonly Regex _spacesPunc = new Regex(@"(?:\s+)(\.|\?)", RegexOptions.Compiled);
711  private IForm<T> _form;
712  private IFields<T> _fields;
713  private TemplateBaseAttribute _annotation;
714  private IRecognize<T> _recognizer;
715  }
716 }
FieldRole
The role the field plays in a form.
Definition: IField.cs:129
The context for the execution of a dialog&#39;s conversational process.
static bool ValidatePattern(IForm< T > form, string pattern, IField< T > field, int argLimit=0)
Validate pattern by ensuring they refer to real fields.
Definition: IPrompt.cs:397
override string ToString()
ToString() override.
Definition: IPrompt.cs:173
Define a template for generating strings.
Definition: Attributes.cs:571
Namespace for the Microsoft Bot Connector SDK.
Prompter(TemplateBaseAttribute annotation, IForm< T > form, IRecognize< T > recognizer, IFields< T > fields=null)
Construct a prompter.
Definition: IPrompt.cs:340
A prompt and recognizer packaged together.
Definition: IPrompt.cs:330
void ApplyDefaults(TemplateBaseAttribute defaultTemplate)
Any default values in this template will be overridden by the supplied defaultTemplate ...
Definition: Attributes.cs:779
string Text
Content for the message
bool AllowsMultiple
Are multiple matches allowed.
Definition: IField.cs:211
TemplateAttribute Template(TemplateUsage usage)
Return a template for building a prompt.
Interface for recognizers that look for matches in user input.
Definition: IRecognize.cs:159
TemplateUsage
All of the built-in templates.
Definition: Attributes.cs:321
abstract IFields< T > Fields
Fields that make up form.
Definition: IForm.cs:53
string Image
URL of image to use when creating cards or buttons.
Definition: Attributes.cs:82
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
ChoiceStyleOptions
Specifies how to show choices generated by {||} in a Pattern Language string.
Definition: Attributes.cs:160
Interface for a prompt and its associated recognizer.
Definition: IPrompt.cs:56
BoolDefault
Three state boolean value.
Definition: Attributes.cs:240
static bool GenerateMessages(this FormPrompt prompt, IMessageActivity preamble, IMessageActivity promptMessage)
Given a prompt definition generate messages to send back.
Definition: IPrompt.cs:248
FormPrompt Prompt(T state, IField< T > field, params object[] args)
Return prompt to send to user.
Definition: IPrompt.cs:357
Namespace for resources.
Form definition interface.
Definition: IForm.cs:47
static string BuildList(IEnumerable< string > values, string separator, string lastSeparator)
Given a list of string values generate a proper English list.
Definition: Language.cs:283
Abstract base class used by all attributes that use Pattern Language.
Definition: Attributes.cs:677
IPrompt< T > Prompt
Returns the prompt description.
Definition: IField.cs:354
const string ImBack
Client will post message to bot, so all other participants will see that was posted to the bot and wh...
Definition: ActionTypes.cs:19
DescribeAttribute Description
Description information for generating cards.
Definition: IPrompt.cs:96
static string ANormalization(string input)
Switch &#39;a&#39; before consonants and &#39;an&#39; before vowels.
Definition: Language.cs:247
IList< Attachment > Attachments
Attachments
bool IsUnknown(T state)
Test to see if the field value form state has a value.
string Name
Name of this field.
Definition: IField.cs:415
object Clone()
Clone the FormButton
Definition: IPrompt.cs:158
static IList< Attachment > GenerateHeroCard(this FormPrompt prompt)
Generate a hero card from a FormPrompt.
Definition: IPrompt.cs:197
ChoiceStyleOptions Style
Desired prompt style.
Definition: IPrompt.cs:101
A Form button that will be mapped to Connector.Action.
Definition: IPrompt.cs:132
object Clone()
Deep clone the FormPrompt.
Definition: IPrompt.cs:117
bool Optional
Test to see if field is optional which means that an unknown value is legal.
Definition: IField.cs:93
Core namespace for Dialogs and associated infrastructure.
static string Normalize(string value, CaseNormalization normalization)
Normalize a string.
Definition: Language.cs:304
static IList< Attachment > GenerateHeroCards(this FormPrompt prompt)
Generate a list of hero cards from a prompt definition.
Definition: IPrompt.cs:223
A Hero card (card with a single, large image)
Root namespace for the Microsoft Bot Builder SDK.
string AttachmentLayout
Hint for how to deal with multiple attachments: [list|carousel] Default:list
Attribute to override the default description of a field, property or enum value. ...
Definition: Attributes.cs:62
The prompt that is returned by form prompter.
Definition: IPrompt.cs:85