├── .gitattributes ├── .gitignore ├── Documentation └── Screenshots │ ├── Scenario1.png │ ├── Scenario2.png │ └── Scenario3.png ├── DynamicFormFlowSample.sln ├── DynamicFormFlowSample ├── App_Start │ └── WebApiConfig.cs ├── Controllers │ └── MessagesController.cs ├── Data │ └── SpaceshipData.cs ├── Dialogs │ └── SpaceshipSelectionDialog.cs ├── DynamicFormFlowSample.csproj ├── Forms │ └── SpaceshipSelectionForm.cs ├── Global.asax ├── Global.asax.cs ├── Models │ └── Spaceship.cs ├── Properties │ └── AssemblyInfo.cs ├── Web.Debug.config ├── Web.Release.config ├── Web.config ├── default.htm └── packages.config ├── LICENSE └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | *.VC.VC.opendb 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.sap 91 | 92 | # TFS 2012 Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding add-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # NCrunch 113 | _NCrunch_* 114 | .*crunch*.local.xml 115 | nCrunchTemp_* 116 | 117 | # MightyMoose 118 | *.mm.* 119 | AutoTest.Net/ 120 | 121 | # Web workbench (sass) 122 | .sass-cache/ 123 | 124 | # Installshield output folder 125 | [Ee]xpress/ 126 | 127 | # DocProject is a documentation generator add-in 128 | DocProject/buildhelp/ 129 | DocProject/Help/*.HxT 130 | DocProject/Help/*.HxC 131 | DocProject/Help/*.hhc 132 | DocProject/Help/*.hhk 133 | DocProject/Help/*.hhp 134 | DocProject/Help/Html2 135 | DocProject/Help/html 136 | 137 | # Click-Once directory 138 | publish/ 139 | 140 | # Publish Web Output 141 | *.[Pp]ublish.xml 142 | *.azurePubxml 143 | # TODO: Comment the next line if you want to checkin your web deploy settings 144 | # but database connection strings (with potential passwords) will be unencrypted 145 | *.pubxml 146 | *.publishproj 147 | 148 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 149 | # checkin your Azure Web App publish settings, but sensitive information contained 150 | # in these scripts will be unencrypted 151 | PublishScripts/ 152 | 153 | # NuGet Packages 154 | *.nupkg 155 | # The packages folder can be ignored because of Package Restore 156 | **/packages/* 157 | # except build/, which is used as an MSBuild target. 158 | !**/packages/build/ 159 | # Uncomment if necessary however generally it will be regenerated when needed 160 | #!**/packages/repositories.config 161 | # NuGet v3's project.json files produces more ignoreable files 162 | *.nuget.props 163 | *.nuget.targets 164 | 165 | # Microsoft Azure Build Output 166 | csx/ 167 | *.build.csdef 168 | 169 | # Microsoft Azure Emulator 170 | ecf/ 171 | rcf/ 172 | 173 | # Windows Store app package directories and files 174 | AppPackages/ 175 | BundleArtifacts/ 176 | Package.StoreAssociation.xml 177 | _pkginfo.txt 178 | 179 | # Visual Studio cache files 180 | # files ending in .cache can be ignored 181 | *.[Cc]ache 182 | # but keep track of directories ending in .cache 183 | !*.[Cc]ache/ 184 | 185 | # Others 186 | ClientBin/ 187 | ~$* 188 | *~ 189 | *.dbmdl 190 | *.dbproj.schemaview 191 | *.pfx 192 | *.publishsettings 193 | node_modules/ 194 | orleans.codegen.cs 195 | 196 | # Since there are multiple workflows, uncomment next line to ignore bower_components 197 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 198 | #bower_components/ 199 | 200 | # RIA/Silverlight projects 201 | Generated_Code/ 202 | 203 | # Backup & report files from converting an old project file 204 | # to a newer Visual Studio version. Backup files are not needed, 205 | # because we have git ;-) 206 | _UpgradeReport_Files/ 207 | Backup*/ 208 | UpgradeLog*.XML 209 | UpgradeLog*.htm 210 | 211 | # SQL Server files 212 | *.mdf 213 | *.ldf 214 | 215 | # Business Intelligence projects 216 | *.rdl.data 217 | *.bim.layout 218 | *.bim_*.settings 219 | 220 | # Microsoft Fakes 221 | FakesAssemblies/ 222 | 223 | # GhostDoc plugin setting file 224 | *.GhostDoc.xml 225 | 226 | # Node.js Tools for Visual Studio 227 | .ntvs_analysis.dat 228 | 229 | # Visual Studio 6 build log 230 | *.plg 231 | 232 | # Visual Studio 6 workspace options file 233 | *.opt 234 | 235 | # Visual Studio LightSwitch build output 236 | **/*.HTMLClient/GeneratedArtifacts 237 | **/*.DesktopClient/GeneratedArtifacts 238 | **/*.DesktopClient/ModelManifest.xml 239 | **/*.Server/GeneratedArtifacts 240 | **/*.Server/ModelManifest.xml 241 | _Pvt_Extensions 242 | 243 | # Paket dependency manager 244 | .paket/paket.exe 245 | paket-files/ 246 | 247 | # FAKE - F# Make 248 | .fake/ 249 | 250 | # JetBrains Rider 251 | .idea/ 252 | *.sln.iml 253 | 254 | # Web config 255 | #*.config 256 | -------------------------------------------------------------------------------- /Documentation/Screenshots/Scenario1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tompaana/dynamic-form-flow-sample/5eed449497fc0c14232635204090a7ab647476e4/Documentation/Screenshots/Scenario1.png -------------------------------------------------------------------------------- /Documentation/Screenshots/Scenario2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tompaana/dynamic-form-flow-sample/5eed449497fc0c14232635204090a7ab647476e4/Documentation/Screenshots/Scenario2.png -------------------------------------------------------------------------------- /Documentation/Screenshots/Scenario3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tompaana/dynamic-form-flow-sample/5eed449497fc0c14232635204090a7ab647476e4/Documentation/Screenshots/Scenario3.png -------------------------------------------------------------------------------- /DynamicFormFlowSample.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25420.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DynamicFormFlowSample", "DynamicFormFlowSample\DynamicFormFlowSample.csproj", "{A8BA1066-5695-4D71-ABB4-65E5A5E0C3D4}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {A8BA1066-5695-4D71-ABB4-65E5A5E0C3D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {A8BA1066-5695-4D71-ABB4-65E5A5E0C3D4}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {A8BA1066-5695-4D71-ABB4-65E5A5E0C3D4}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {A8BA1066-5695-4D71-ABB4-65E5A5E0C3D4}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /DynamicFormFlowSample/App_Start/WebApiConfig.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Serialization; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Web.Http; 7 | 8 | namespace DynamicFormFlowSample 9 | { 10 | public static class WebApiConfig 11 | { 12 | public static void Register(HttpConfiguration config) 13 | { 14 | // Json settings 15 | config.Formatters.JsonFormatter.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; 16 | config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); 17 | config.Formatters.JsonFormatter.SerializerSettings.Formatting = Formatting.Indented; 18 | JsonConvert.DefaultSettings = () => new JsonSerializerSettings() 19 | { 20 | ContractResolver = new CamelCasePropertyNamesContractResolver(), 21 | Formatting = Newtonsoft.Json.Formatting.Indented, 22 | NullValueHandling = NullValueHandling.Ignore, 23 | }; 24 | 25 | // Web API configuration and services 26 | 27 | // Web API routes 28 | config.MapHttpAttributeRoutes(); 29 | 30 | config.Routes.MapHttpRoute( 31 | name: "DefaultApi", 32 | routeTemplate: "api/{controller}/{id}", 33 | defaults: new { id = RouteParameter.Optional } 34 | ); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /DynamicFormFlowSample/Controllers/MessagesController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | using System.Web.Http; 6 | using Microsoft.Bot.Connector; 7 | using Microsoft.Bot.Builder.Dialogs; 8 | using DynamicFormFlowSample.Models; 9 | using DynamicFormFlowSample.Dialogs; 10 | using DynamicFormFlowSample.Forms; 11 | 12 | namespace DynamicFormFlowSample 13 | { 14 | [BotAuthentication] 15 | public class MessagesController : ApiController 16 | { 17 | internal static IDialog MakeRootDialog() 18 | { 19 | return Chain.From(() => new SpaceshipSelectionDialog(SpaceshipSelectionForm.BuildForm)); 20 | } 21 | 22 | public async Task Post([FromBody]Activity activity) 23 | { 24 | if (activity.Type == ActivityTypes.Message) 25 | { 26 | try 27 | { 28 | await Conversation.SendAsync(activity, MakeRootDialog); 29 | } 30 | catch (Exception e) 31 | { 32 | System.Diagnostics.Debug.WriteLine(e.Message); 33 | } 34 | } 35 | else 36 | { 37 | HandleSystemMessage(activity); 38 | } 39 | var response = Request.CreateResponse(HttpStatusCode.OK); 40 | return response; 41 | } 42 | 43 | private Activity HandleSystemMessage(Activity message) 44 | { 45 | if (message.Type == ActivityTypes.DeleteUserData) 46 | { 47 | // Implement user deletion here 48 | // If we handle user deletion, return a real message 49 | } 50 | else if (message.Type == ActivityTypes.ConversationUpdate) 51 | { 52 | // Handle conversation state changes, like members being added and removed 53 | // Use Activity.MembersAdded and Activity.MembersRemoved and Activity.Action for info 54 | // Not available in all channels 55 | } 56 | else if (message.Type == ActivityTypes.ContactRelationUpdate) 57 | { 58 | // Handle add/remove from contact lists 59 | // Activity.From + Activity.Action represent what happened 60 | } 61 | else if (message.Type == ActivityTypes.Typing) 62 | { 63 | // Handle knowing tha the user is typing 64 | } 65 | else if (message.Type == ActivityTypes.Ping) 66 | { 67 | } 68 | 69 | return null; 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /DynamicFormFlowSample/Data/SpaceshipData.cs: -------------------------------------------------------------------------------- 1 | using DynamicFormFlowSample.Models; 2 | using System.Collections.Generic; 3 | 4 | namespace DynamicFormFlowSample.Data 5 | { 6 | /// 7 | /// Represents the data in an imaginary database. 8 | /// 9 | /// Note that normally this data would be retrieved online, but for the sake of simplicity of 10 | /// this sample, the dummy data here is local. 11 | /// 12 | public class SpaceshipData 13 | { 14 | private static SpaceshipData _instance; 15 | public static SpaceshipData Instance 16 | { 17 | get 18 | { 19 | if (_instance == null) 20 | { 21 | _instance = new SpaceshipData(); 22 | } 23 | 24 | return _instance; 25 | } 26 | } 27 | 28 | public IList Spaceships 29 | { 30 | get; 31 | private set; 32 | } 33 | 34 | /// 35 | /// NOTE: This data should be session and user specific! 36 | /// For the sake of simplicity this data is local here, but in real-life scenario this 37 | /// would cause issues: Multiple user accessing the data simultaneously will lead to 38 | /// unwanted results. 39 | /// 40 | public IList LastSearchResults 41 | { 42 | get; 43 | set; 44 | } 45 | 46 | private SpaceshipData() 47 | { 48 | Spaceships = new List(); 49 | CreateDummyData(); 50 | } 51 | 52 | /// 53 | /// Looks for (partially) matching spaceships from the data based on the given criteria. 54 | /// 55 | /// The filter for the search. 56 | /// A list of partial matches or an empty list if none found. 57 | public IList SearchForPartialMatches(Spaceship spaceshipFilter) 58 | { 59 | IList partialMatches = new List(); 60 | 61 | if (spaceshipFilter != null) 62 | { 63 | foreach (Spaceship spaceship in Spaceships) 64 | { 65 | if (spaceship.IsPartialMatch(spaceshipFilter)) 66 | { 67 | partialMatches.Add(spaceship); 68 | } 69 | } 70 | } 71 | 72 | return partialMatches; 73 | } 74 | 75 | private void CreateDummyData() 76 | { 77 | // Star Trek 78 | Spaceships.Add(new Spaceship() 79 | { 80 | Size = Spaceship.Sizes.Small, 81 | Engines = new List() { Spaceship.EngineTypes.Inpulse }, 82 | Weapons = new List() { Spaceship.WeaponTypes.Phaser, Spaceship.WeaponTypes.PhotonTorpedos }, 83 | Crew = Spaceship.CrewTypes.GoodGuys, 84 | Name = "Federation attack fighter", 85 | ImageUri = "https://upload.wikimedia.org/wikipedia/en/thumb/9/9f/Star_Trek_movie_logo_2009.jpg/310px-Star_Trek_movie_logo_2009.jpg" 86 | }); 87 | Spaceships.Add(new Spaceship() 88 | { 89 | Size = Spaceship.Sizes.Small, 90 | Engines = new List() { Spaceship.EngineTypes.Inpulse, Spaceship.EngineTypes.Warp }, 91 | Weapons = new List() { Spaceship.WeaponTypes.None }, 92 | Crew = Spaceship.CrewTypes.GoodGuys, 93 | Name = "NX Alpha", 94 | ImageUri = "https://upload.wikimedia.org/wikipedia/en/thumb/9/9f/Star_Trek_movie_logo_2009.jpg/310px-Star_Trek_movie_logo_2009.jpg" 95 | }); 96 | Spaceships.Add(new Spaceship() 97 | { 98 | Size = Spaceship.Sizes.Large, 99 | Engines = new List() { Spaceship.EngineTypes.Inpulse, Spaceship.EngineTypes.Warp }, 100 | Weapons = new List() { Spaceship.WeaponTypes.Phaser, Spaceship.WeaponTypes.PhotonTorpedos }, 101 | Crew = Spaceship.CrewTypes.GoodGuys, 102 | Name = "Enterprise (NX-01)", 103 | ImageUri = "https://upload.wikimedia.org/wikipedia/en/thumb/9/9f/Star_Trek_movie_logo_2009.jpg/310px-Star_Trek_movie_logo_2009.jpg" 104 | }); 105 | Spaceships.Add(new Spaceship() 106 | { 107 | Size = Spaceship.Sizes.Large, 108 | Engines = new List() { Spaceship.EngineTypes.Inpulse, Spaceship.EngineTypes.Warp }, 109 | Weapons = new List() { Spaceship.WeaponTypes.Phaser, Spaceship.WeaponTypes.PhotonTorpedos }, 110 | Crew = Spaceship.CrewTypes.GoodGuys, 111 | Name = "USS Franklin (NX-326)", 112 | ImageUri = "https://upload.wikimedia.org/wikipedia/en/thumb/9/9f/Star_Trek_movie_logo_2009.jpg/310px-Star_Trek_movie_logo_2009.jpg" 113 | }); 114 | Spaceships.Add(new Spaceship() 115 | { 116 | Size = Spaceship.Sizes.Large, 117 | Engines = new List() { Spaceship.EngineTypes.Inpulse, Spaceship.EngineTypes.Warp }, 118 | Weapons = new List() { Spaceship.WeaponTypes.Disruptor, Spaceship.WeaponTypes.PhotonTorpedos }, 119 | Crew = Spaceship.CrewTypes.BadGuys, 120 | Name = "IKS Toh'Kaht (Klingon attack cruiser)", 121 | ImageUri = "https://upload.wikimedia.org/wikipedia/en/thumb/9/9f/Star_Trek_movie_logo_2009.jpg/310px-Star_Trek_movie_logo_2009.jpg" 122 | }); 123 | Spaceships.Add(new Spaceship() 124 | { 125 | Size = Spaceship.Sizes.Large, 126 | Engines = new List() { Spaceship.EngineTypes.Inpulse, Spaceship.EngineTypes.Warp }, 127 | Weapons = new List() { Spaceship.WeaponTypes.Disruptor, Spaceship.WeaponTypes.PhotonTorpedos }, 128 | Crew = Spaceship.CrewTypes.BadGuys, 129 | Name = "Scimitar (Reman warbird)", 130 | ImageUri = "https://upload.wikimedia.org/wikipedia/en/thumb/9/9f/Star_Trek_movie_logo_2009.jpg/310px-Star_Trek_movie_logo_2009.jpg" 131 | }); 132 | 133 | // Star Wars 134 | Spaceships.Add(new Spaceship() 135 | { 136 | Size = Spaceship.Sizes.Small, 137 | Engines = new List() { Spaceship.EngineTypes.Sublight }, 138 | Weapons = new List() { Spaceship.WeaponTypes.Laser, Spaceship.WeaponTypes.ProtonTorpedos }, 139 | Crew = Spaceship.CrewTypes.GoodGuys, 140 | Name = "X-Wing", 141 | ImageUri = "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6c/Star_Wars_Logo.svg/1280px-Star_Wars_Logo.svg.png" 142 | }); 143 | Spaceships.Add(new Spaceship() 144 | { 145 | Size = Spaceship.Sizes.Mid, 146 | Engines = new List() { Spaceship.EngineTypes.Sublight, Spaceship.EngineTypes.Hyper }, 147 | Weapons = new List() { Spaceship.WeaponTypes.Laser, Spaceship.WeaponTypes.Missiles }, 148 | Crew = Spaceship.CrewTypes.GoodGuys, 149 | Name = "Millenium Falcon (YT-1300)", 150 | ImageUri = "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6c/Star_Wars_Logo.svg/1280px-Star_Wars_Logo.svg.png" 151 | }); 152 | Spaceships.Add(new Spaceship() 153 | { 154 | Size = Spaceship.Sizes.Large, 155 | Engines = new List() { Spaceship.EngineTypes.Sublight, Spaceship.EngineTypes.Hyper }, 156 | Weapons = new List() { Spaceship.WeaponTypes.Laser, Spaceship.WeaponTypes.Ion }, 157 | Crew = Spaceship.CrewTypes.GoodGuys, 158 | Name = "Home One (MC80)", 159 | ImageUri = "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6c/Star_Wars_Logo.svg/1280px-Star_Wars_Logo.svg.png" 160 | }); 161 | Spaceships.Add(new Spaceship() 162 | { 163 | Size = Spaceship.Sizes.Small, 164 | Engines = new List() { Spaceship.EngineTypes.Sublight }, 165 | Weapons = new List() { Spaceship.WeaponTypes.Laser, Spaceship.WeaponTypes.ProtonTorpedos }, 166 | Crew = Spaceship.CrewTypes.BadGuys, 167 | Name = "Tie Fighter", 168 | ImageUri = "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6c/Star_Wars_Logo.svg/1280px-Star_Wars_Logo.svg.png" 169 | }); 170 | Spaceships.Add(new Spaceship() 171 | { 172 | Size = Spaceship.Sizes.Small, 173 | Engines = new List() { Spaceship.EngineTypes.Sublight, Spaceship.EngineTypes.Hyper }, 174 | Weapons = new List() 175 | { 176 | Spaceship.WeaponTypes.Laser, 177 | Spaceship.WeaponTypes.Ion, 178 | Spaceship.WeaponTypes.ProtonTorpedos, 179 | Spaceship.WeaponTypes.Missiles 180 | }, 181 | Crew = Spaceship.CrewTypes.BadGuys, 182 | Name = "Slave I (Firespray-31-class)", 183 | ImageUri = "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6c/Star_Wars_Logo.svg/1280px-Star_Wars_Logo.svg.png" 184 | }); 185 | Spaceships.Add(new Spaceship() 186 | { 187 | Size = Spaceship.Sizes.Large, 188 | Engines = new List() { Spaceship.EngineTypes.Sublight, Spaceship.EngineTypes.Hyper }, 189 | Weapons = new List() 190 | { 191 | Spaceship.WeaponTypes.Laser, 192 | Spaceship.WeaponTypes.Ion, 193 | Spaceship.WeaponTypes.Missiles 194 | }, 195 | Crew = Spaceship.CrewTypes.BadGuys, 196 | Name = "Star Destroyer (Imperial-class)", 197 | ImageUri = "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6c/Star_Wars_Logo.svg/1280px-Star_Wars_Logo.svg.png" 198 | }); 199 | 200 | // Battlestar Galactica 201 | Spaceships.Add(new Spaceship() 202 | { 203 | Size = Spaceship.Sizes.Small, 204 | Engines = new List() { Spaceship.EngineTypes.TurboThrust }, 205 | Weapons = new List() { Spaceship.WeaponTypes.KineticEnergy, Spaceship.WeaponTypes.Missiles }, 206 | Crew = Spaceship.CrewTypes.GoodGuys, 207 | Name = "Viper Mk VII", 208 | ImageUri = "https://upload.wikimedia.org/wikipedia/en/4/40/Battlestar_Galactica_intro.jpg" 209 | }); 210 | Spaceships.Add(new Spaceship() 211 | { 212 | Size = Spaceship.Sizes.Large, 213 | Engines = new List() { Spaceship.EngineTypes.Sublight, Spaceship.EngineTypes.FTL }, 214 | Weapons = new List() { Spaceship.WeaponTypes.Batteries, Spaceship.WeaponTypes.Missiles }, 215 | Crew = Spaceship.CrewTypes.GoodGuys, 216 | Name = "Battlestar Galactica", 217 | ImageUri = "https://upload.wikimedia.org/wikipedia/en/4/40/Battlestar_Galactica_intro.jpg" 218 | }); 219 | Spaceships.Add(new Spaceship() 220 | { 221 | Size = Spaceship.Sizes.Small, 222 | Engines = new List() { Spaceship.EngineTypes.Sublight }, 223 | Weapons = new List() { Spaceship.WeaponTypes.KineticEnergy, Spaceship.WeaponTypes.Missiles }, 224 | Crew = Spaceship.CrewTypes.BadGuys, 225 | Name = "Cylon Raider", 226 | ImageUri = "https://upload.wikimedia.org/wikipedia/en/4/40/Battlestar_Galactica_intro.jpg" 227 | }); 228 | Spaceships.Add(new Spaceship() 229 | { 230 | Size = Spaceship.Sizes.Large, 231 | Engines = new List() { Spaceship.EngineTypes.Sublight, Spaceship.EngineTypes.FTL }, 232 | Weapons = new List() { Spaceship.WeaponTypes.Missiles }, 233 | Crew = Spaceship.CrewTypes.BadGuys, 234 | Name = "Cylon Basestar", 235 | ImageUri = "https://upload.wikimedia.org/wikipedia/en/4/40/Battlestar_Galactica_intro.jpg" 236 | }); 237 | } 238 | } 239 | } -------------------------------------------------------------------------------- /DynamicFormFlowSample/Dialogs/SpaceshipSelectionDialog.cs: -------------------------------------------------------------------------------- 1 | using DynamicFormFlowSample.Models; 2 | using Microsoft.Bot.Builder.Dialogs; 3 | using System; 4 | using System.Threading.Tasks; 5 | using Microsoft.Bot.Builder.FormFlow; 6 | using DynamicFormFlowSample.Data; 7 | using System.Collections.Generic; 8 | using Microsoft.Bot.Connector; 9 | using System.Linq; 10 | 11 | namespace DynamicFormFlowSample.Dialogs 12 | { 13 | [Serializable] 14 | public class SpaceshipSelectionDialog : IDialog 15 | { 16 | private readonly BuildFormDelegate BuildSpaceshipSelectionForm; 17 | private IList _spaceshipMatches; 18 | 19 | internal SpaceshipSelectionDialog(BuildFormDelegate buildSpaceshipSelectionForm) 20 | { 21 | BuildSpaceshipSelectionForm = buildSpaceshipSelectionForm; 22 | } 23 | 24 | #pragma warning disable 1998 25 | public async Task StartAsync(IDialogContext context) 26 | { 27 | _spaceshipMatches = null; 28 | SpaceshipData.Instance.LastSearchResults = null; 29 | 30 | var spaceshipSelectionForm = new FormDialog(new Spaceship(), BuildSpaceshipSelectionForm, FormOptions.None); 31 | context.Call(spaceshipSelectionForm, OnSpaceshipSelectionFormCompleteAsync); 32 | } 33 | #pragma warning restore 1998 34 | 35 | /// 36 | /// Called once the form is complete. The result will have the properties selected by the user. 37 | /// 38 | /// 39 | /// 40 | /// 41 | private async Task OnSpaceshipSelectionFormCompleteAsync(IDialogContext context, IAwaitable result) 42 | { 43 | Spaceship spaceship = null; 44 | 45 | try 46 | { 47 | spaceship = await result; 48 | } 49 | catch (FormCanceledException e) 50 | { 51 | System.Diagnostics.Debug.WriteLine($"Form canceled: {e.Message}"); 52 | } 53 | 54 | if (spaceship != null) 55 | { 56 | System.Diagnostics.Debug.WriteLine($"We've got the criteria for the ship:\n{spaceship.PropertiesAsFormattedString()}"); 57 | SpaceshipData spaceshipData = SpaceshipData.Instance; 58 | _spaceshipMatches = spaceshipData.SearchForPartialMatches(spaceship); 59 | 60 | if (_spaceshipMatches.Count == 1) 61 | { 62 | // Single match for the given property -> Choise is made! 63 | IMessageActivity messageActivity = context.MakeMessage(); 64 | ThumbnailCard thumbnailCard = CreateSpaceshipThumbnailCard(_spaceshipMatches[0]); 65 | messageActivity.Attachments = new List() { thumbnailCard.ToAttachment() }; 66 | messageActivity.Text = $"You've chosen \"{_spaceshipMatches[0].Name}\", well done!"; 67 | await context.PostAsync(messageActivity); 68 | context.Done(_spaceshipMatches[0]); 69 | } 70 | else if (_spaceshipMatches.Count > 1) 71 | { 72 | // More than one match -> Show the available options 73 | await ShowSelectSpaceshipPromptAsync(context); 74 | } 75 | else 76 | { 77 | // Nothing found matching the given criteria 78 | await context.PostAsync("No spaceships found with the given criteria"); 79 | context.Fail(new ArgumentException("No spaceships found with the given criteria")); 80 | } 81 | } 82 | else 83 | { 84 | context.Fail(new NullReferenceException("No spaceship :(")); 85 | } 86 | } 87 | 88 | /// 89 | /// Called when the user selects a spaceship from the shown options. 90 | /// 91 | /// 92 | /// 93 | /// 94 | private async Task OnSpaceshipSelectedAsync(IDialogContext context, IAwaitable result) 95 | { 96 | object awaitedResult = await result; 97 | string spaceshipName = null; 98 | 99 | if (awaitedResult != null) 100 | { 101 | if (awaitedResult is Activity) 102 | { 103 | spaceshipName = (awaitedResult as Activity).Text; 104 | } 105 | else 106 | { 107 | spaceshipName = awaitedResult.ToString(); 108 | } 109 | } 110 | 111 | if (!string.IsNullOrEmpty(spaceshipName)) 112 | { 113 | SpaceshipData spaceshipData = SpaceshipData.Instance; 114 | Spaceship selectedSpaceship = null; 115 | 116 | try 117 | { 118 | selectedSpaceship = spaceshipData.Spaceships.First(spaceship => spaceship.Name.Equals(spaceshipName)); 119 | } 120 | catch (InvalidOperationException) 121 | { 122 | await context.PostAsync($"\"{spaceshipName}\" is not a valid option, please try again"); 123 | } 124 | 125 | if (selectedSpaceship != null) 126 | { 127 | IMessageActivity messageActivity = context.MakeMessage(); 128 | ThumbnailCard thumbnailCard = CreateSpaceshipThumbnailCard(selectedSpaceship); 129 | messageActivity.Attachments = new List() { thumbnailCard.ToAttachment() }; 130 | messageActivity.Text = $"\"{spaceshipName}\" it is, great choice!"; 131 | await context.PostAsync(messageActivity); 132 | context.Done(selectedSpaceship); 133 | } 134 | else 135 | { 136 | await ShowSelectSpaceshipPromptAsync(context); 137 | } 138 | } 139 | } 140 | 141 | /// 142 | /// Displays the available spaceship options based on the search done after the form was 143 | /// completed. 144 | /// 145 | /// 146 | /// 147 | private async Task ShowSelectSpaceshipPromptAsync(IDialogContext context) 148 | { 149 | if (_spaceshipMatches != null) 150 | { 151 | if (_spaceshipMatches.Count > 1) 152 | { 153 | IMessageActivity messageActivity = context.MakeMessage(); 154 | CreateSpaceshipCarousel(ref messageActivity, _spaceshipMatches); 155 | await context.PostAsync(messageActivity); 156 | context.Wait(OnSpaceshipSelectedAsync); 157 | } 158 | else 159 | { 160 | context.Fail(new ArgumentException($"Need to have at least two spaceships ({nameof(_spaceshipMatches)})")); 161 | } 162 | } 163 | else 164 | { 165 | context.Fail(new NullReferenceException($"No list of matching spaceships ({nameof(_spaceshipMatches)})")); 166 | } 167 | } 168 | 169 | /// 170 | /// Creates a thumbnail card for the given spaceship. 171 | /// 172 | /// 173 | /// A newly created thumbnail card. 174 | private ThumbnailCard CreateSpaceshipThumbnailCard(Spaceship spaceship) 175 | { 176 | ThumbnailCard thumbnailCard = null; 177 | 178 | if (spaceship != null) 179 | { 180 | thumbnailCard = new ThumbnailCard() 181 | { 182 | Title = spaceship.Name, 183 | Images = new[] { new CardImage(spaceship.ImageUri) }, 184 | Text = spaceship.PropertiesAsFormattedString() 185 | }; 186 | } 187 | 188 | return thumbnailCard; 189 | } 190 | 191 | /// 192 | /// Creates a carousel with the given spaceships and inserts that into the given message activity. 193 | /// 194 | /// The message activity to insert the carousel into. 195 | private void CreateSpaceshipCarousel(ref IMessageActivity messageActivity, IList spaceships) 196 | { 197 | IList thumbnailCards = new List(); 198 | 199 | foreach (Spaceship spaceship in spaceships) 200 | { 201 | ThumbnailCard thumbnailCard = CreateSpaceshipThumbnailCard(spaceship); 202 | thumbnailCard.Buttons = 203 | new[] { new CardAction(type: ActionTypes.PostBack, title: "Select", value: spaceship.Name) }; 204 | thumbnailCards.Add(thumbnailCard); 205 | } 206 | 207 | messageActivity.AttachmentLayout = AttachmentLayoutTypes.Carousel; 208 | messageActivity.Attachments = thumbnailCards.Select(thumbnailCard => thumbnailCard.ToAttachment()).ToList(); 209 | messageActivity.Text = "Spaceships matching your criteria"; 210 | } 211 | } 212 | } -------------------------------------------------------------------------------- /DynamicFormFlowSample/DynamicFormFlowSample.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | 8 | 9 | 2.0 10 | {A8BA1066-5695-4D71-ABB4-65E5A5E0C3D4} 11 | {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} 12 | Library 13 | Properties 14 | DynamicFormFlowSample 15 | Bot Application1 16 | v4.6 17 | true 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | true 29 | full 30 | false 31 | bin\ 32 | DEBUG;TRACE 33 | prompt 34 | 4 35 | 36 | 37 | pdbonly 38 | true 39 | bin\ 40 | TRACE 41 | prompt 42 | 4 43 | 44 | 45 | 46 | ..\packages\Autofac.3.5.2\lib\net40\Autofac.dll 47 | True 48 | 49 | 50 | ..\packages\Chronic.Signed.0.3.2\lib\net40\Chronic.dll 51 | True 52 | 53 | 54 | ..\packages\Microsoft.Bot.Builder.3.0.0\lib\net46\Microsoft.Bot.Builder.dll 55 | True 56 | 57 | 58 | ..\packages\Microsoft.Bot.Builder.3.0.0\lib\net46\Microsoft.Bot.Connector.dll 59 | True 60 | 61 | 62 | 63 | ..\packages\Microsoft.IdentityModel.Protocol.Extensions.1.0.2.206221351\lib\net45\Microsoft.IdentityModel.Protocol.Extensions.dll 64 | True 65 | 66 | 67 | ..\packages\Microsoft.Rest.ClientRuntime.1.8.2\lib\net45\Microsoft.Rest.ClientRuntime.dll 68 | True 69 | 70 | 71 | ..\packages\Microsoft.WindowsAzure.ConfigurationManager.3.1.0\lib\net40\Microsoft.WindowsAzure.Configuration.dll 72 | True 73 | 74 | 75 | ..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll 76 | True 77 | 78 | 79 | 80 | ..\packages\System.IdentityModel.Tokens.Jwt.4.0.2.206221351\lib\net45\System.IdentityModel.Tokens.Jwt.dll 81 | True 82 | 83 | 84 | 85 | 86 | ..\packages\Microsoft.AspNet.WebApi.Client.5.2.3\lib\net45\System.Net.Http.Formatting.dll 87 | True 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | ..\packages\Microsoft.AspNet.WebApi.Core.5.2.3\lib\net45\System.Web.Http.dll 101 | True 102 | 103 | 104 | ..\packages\Microsoft.AspNet.WebApi.WebHost.5.2.3\lib\net45\System.Web.Http.WebHost.dll 105 | True 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | Designer 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | Global.asax 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | Web.config 136 | 137 | 138 | Web.config 139 | 140 | 141 | 142 | 143 | 10.0 144 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 145 | 146 | 147 | true 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | True 157 | True 158 | 3979 159 | / 160 | http://localhost:3979/ 161 | False 162 | False 163 | 164 | 165 | False 166 | 167 | 168 | 169 | 170 | 177 | -------------------------------------------------------------------------------- /DynamicFormFlowSample/Forms/SpaceshipSelectionForm.cs: -------------------------------------------------------------------------------- 1 | using DynamicFormFlowSample.Data; 2 | using DynamicFormFlowSample.Models; 3 | using Microsoft.Bot.Builder.FormFlow; 4 | using Microsoft.Bot.Builder.FormFlow.Advanced; 5 | using System.Collections.Generic; 6 | using System.Globalization; 7 | using System.Threading.Tasks; 8 | 9 | namespace DynamicFormFlowSample.Forms 10 | { 11 | public class SpaceshipSelectionForm 12 | { 13 | public static IForm BuildForm() 14 | { 15 | var builder = new FormBuilder(); 16 | 17 | return builder 18 | // The following is the simple way of creating a field - you can leave out the 19 | // validation, if it not needed. Hint: Use Prompt attribute in the property; 20 | // see the Size property in Spaceship class. 21 | .Field(nameof(Spaceship.Size), 22 | validate: async (state, value) => 23 | await ValidateResponseAsync(value, state, nameof(Spaceship.Size))) 24 | 25 | // The following way of creating a field provides the best access to define its 26 | // behavior: 27 | // 28 | // - SetType: If you want to explicitly define the type of the property 29 | // - SetActive: Should we present this query? 30 | // - SetDefine: What values will be available? 31 | // - SetPrompt: How do we present the query? 32 | // - SetValidate: The actions taken after response 33 | // 34 | .Field(new FieldReflector(nameof(Spaceship.Engines)) 35 | .SetType(typeof(Spaceship.EngineTypes)) 36 | .SetActive((state) => SetFieldActive(state, nameof(Spaceship.Engines))) 37 | .SetDefine(async (state, field) => await SetOptionsForFieldsAsync(state, nameof(Spaceship.Engines), field)) 38 | .SetAllowsMultiple(true) 39 | .SetPrompt(new PromptAttribute("What type of engines does the ship have? {||}")) 40 | .SetValidate(async (state, value) => await ValidateResponseAsync(value, state, nameof(Spaceship.Engines)))) 41 | 42 | // You could also replace the field above with the following, if you have no need 43 | // to define the values for the field or validate the response: 44 | //.Field("Engines") 45 | 46 | // Below we want only to allow one option. To achieve that we've defined a 47 | // different property where the value is stored in (Spaceship.Weapon instead 48 | // of Spaceship.Weapons). 49 | .Field(new FieldReflector(nameof(Spaceship.Weapon)) 50 | .SetActive((state) => SetFieldActive(state, nameof(Spaceship.Weapons))) 51 | .SetDefine(async (state, field) => await SetOptionsForFieldsAsync(state, nameof(Spaceship.Weapons), field)) 52 | .SetPrompt(new PromptAttribute("How about the weapons on the ship? {||}") 53 | { 54 | ChoiceStyle = ChoiceStyleOptions.Default 55 | }) 56 | .SetValidate(async (state, value) => await ValidateResponseAsync(value, state, nameof(Spaceship.Weapons)))) 57 | 58 | .Field(new FieldReflector(nameof(Spaceship.Crew)) 59 | .SetActive((state) => SetFieldActive(state, nameof(Spaceship.Crew))) 60 | .SetDefine(async (state, field) => await SetOptionsForFieldsAsync(state, nameof(Spaceship.Crew), field)) 61 | .SetPrompt(new PromptAttribute("What kind of crew typically runs the ship? {||}")) 62 | .SetValidate(async (state, value) => await ValidateResponseAsync(value, state, nameof(Spaceship.Crew)))) 63 | 64 | // Uncomment the following to present a confirmation prompt 65 | //.Confirm(generateMessage: (state) => GenerateConfirmMessageAsync(state)) 66 | 67 | .Build(); 68 | } 69 | 70 | #pragma warning disable 1998 71 | /// 72 | /// Validates the response and does a new search with the updated parameters. 73 | /// 74 | /// The response from the user. 75 | /// The current state of the form (what details we have gathered to our spaceship). 76 | /// The name of the property queried. 77 | /// The validation result. 78 | private static async Task ValidateResponseAsync( 79 | object response, Spaceship spaceshipState, string propertyName) 80 | { 81 | Spaceship spaceshipSearchFilter = new Spaceship(spaceshipState); 82 | object value = Spaceship.VerifyPropertyValue(response, propertyName); 83 | SpaceshipData spaceshipData = SpaceshipData.Instance; 84 | IList matches = null; 85 | 86 | if (spaceshipSearchFilter.SetPropertyValue(value, propertyName)) 87 | { 88 | matches = spaceshipData.SearchForPartialMatches(spaceshipSearchFilter); 89 | } 90 | 91 | bool isValid = (matches != null && matches.Count > 0); 92 | 93 | ValidateResult validateResult = new ValidateResult 94 | { 95 | IsValid = (isValid && value != null), 96 | Value = value 97 | }; 98 | 99 | string feedbackMessage = string.Empty; 100 | 101 | if (!isValid) 102 | { 103 | // Since this was an invalid option, undo the change 104 | spaceshipState.ClearPropertyValue(propertyName); 105 | 106 | string valueAsString = ValueToString(value); 107 | System.Diagnostics.Debug.WriteLine($"Value {valueAsString} for property {propertyName} is invalid"); 108 | feedbackMessage = $"\"{valueAsString}\" is not a valid option"; 109 | } 110 | else 111 | { 112 | // Store the search 113 | spaceshipData.LastSearchResults = matches; 114 | } 115 | 116 | if (matches != null && matches.Count > 5) 117 | { 118 | feedbackMessage = $"Still {matches.Count} options matching your criteria. Let's get some more details!"; 119 | } 120 | 121 | if (!string.IsNullOrEmpty(feedbackMessage)) 122 | { 123 | System.Diagnostics.Debug.WriteLine(feedbackMessage); 124 | validateResult.Feedback = feedbackMessage; 125 | } 126 | 127 | return validateResult; 128 | } 129 | #pragma warning restore 1998 130 | 131 | /// 132 | /// Checks whether the given field should be active or not. 133 | /// 134 | /// The current state i.e. details for the watch filled so far. 135 | /// The name of the field to check. 136 | /// True, if the given field should be active (has values). False otherwise. 137 | private static bool SetFieldActive(Spaceship spaceshipState, string propertyName) 138 | { 139 | bool setActive = true; 140 | SpaceshipData spaceshipData = SpaceshipData.Instance; 141 | 142 | if (spaceshipData.LastSearchResults != null) 143 | { 144 | if (spaceshipData.LastSearchResults.Count < 2) 145 | { 146 | // There's only one or no options left - it makes no sense to ask anymore questions 147 | setActive = false; 148 | } 149 | 150 | if (OptionsLeftForProperty(spaceshipData.LastSearchResults, propertyName).Count < 2) 151 | { 152 | // There's only one or no options left for the given property - again this question is pointless 153 | setActive = false; 154 | } 155 | } 156 | else 157 | { 158 | System.Diagnostics.Debug.WriteLine($"Failed to check if not to set the field active for property {propertyName} due to missing search results"); 159 | } 160 | 161 | System.Diagnostics.Debug.WriteLine($"Set field active for property {propertyName}: {setActive}"); 162 | return setActive; 163 | } 164 | 165 | #pragma warning disable 1998 166 | /// 167 | /// Sets the values for the given field. 168 | /// 169 | /// The current state i.e. details for the watch filled so far. 170 | /// The name of the property (field). 171 | /// The field to populate. 172 | /// True, if values found and field populated. False otherwise. 173 | private static async Task SetOptionsForFieldsAsync( 174 | Spaceship spaceshipState, string propertyName, Field field) 175 | { 176 | bool valuesSet = false; 177 | SpaceshipData spaceshipData = SpaceshipData.Instance; 178 | 179 | if (spaceshipData.LastSearchResults != null) 180 | { 181 | // Clear the values to avoid duplicates since this method can be called many times 182 | field.RemoveValues(); 183 | 184 | IList values = OptionsLeftForProperty(spaceshipData.LastSearchResults, propertyName); 185 | 186 | foreach (object value in values) 187 | { 188 | System.Diagnostics.Debug.WriteLine($"Adding value {value} for property {propertyName} to field {field.Name}"); 189 | string valueInTitleCase = CamelCaseToTitleCase(value.ToString()); 190 | 191 | field 192 | .AddDescription(value, valueInTitleCase) 193 | .AddTerms(value, valueInTitleCase); 194 | 195 | valuesSet = true; 196 | } 197 | } 198 | else 199 | { 200 | System.Diagnostics.Debug.WriteLine($"Failed to set values for property {propertyName} due to missing search results"); 201 | } 202 | 203 | return valuesSet; 204 | } 205 | #pragma warning restore 1998 206 | 207 | #pragma warning disable 1998 208 | private static async Task GenerateConfirmMessageAsync(Spaceship spaceshipState) 209 | { 210 | return new PromptAttribute("Is this correct: " + spaceshipState.ToString() + "? {||}") 211 | { 212 | ChoiceStyle = ChoiceStyleOptions.Default 213 | }; 214 | } 215 | #pragma warning restore 1998 216 | 217 | /// 218 | /// Checks the options for the given property left in the given list of spaceships. 219 | /// 220 | /// A list of spaceships to check the options from. 221 | /// The name of the property whose options to check. 222 | /// The options (values for property) as a list. 223 | private static IList OptionsLeftForProperty(IList spaceshipsLeft, string propertyName) 224 | { 225 | IList options = new List(); 226 | 227 | foreach (Spaceship spaceship in spaceshipsLeft) 228 | { 229 | if (propertyName.Equals(nameof(Spaceship.Size))) 230 | { 231 | if (spaceship.Size != Spaceship.Sizes.NotDefined 232 | && !options.Contains(spaceship.Size.ToString())) 233 | { 234 | options.Add(spaceship.Size); 235 | } 236 | } 237 | else if (propertyName.Equals(nameof(Spaceship.Engines))) 238 | { 239 | foreach (Spaceship.EngineTypes engineType in spaceship.Engines) 240 | { 241 | if (engineType != Spaceship.EngineTypes.NotDefined 242 | && !options.Contains(engineType.ToString())) 243 | { 244 | options.Add(engineType); 245 | } 246 | } 247 | } 248 | else if (propertyName.Equals(nameof(Spaceship.Weapons))) 249 | { 250 | foreach (Spaceship.WeaponTypes weaponType in spaceship.Weapons) 251 | { 252 | if (weaponType != Spaceship.WeaponTypes.NotDefined 253 | && !options.Contains(weaponType.ToString())) 254 | { 255 | options.Add(weaponType); 256 | } 257 | } 258 | } 259 | else if (propertyName.Equals(nameof(Spaceship.Crew))) 260 | { 261 | if (spaceship.Crew != Spaceship.CrewTypes.NotDefined 262 | && !options.Contains(spaceship.Crew.ToString())) 263 | { 264 | options.Add(spaceship.Crew); 265 | } 266 | } 267 | } 268 | 269 | System.Diagnostics.Debug.WriteLine($"{options.Count} options left for property {propertyName}"); 270 | return options; 271 | } 272 | 273 | /// 274 | /// Formats the given camel case string to title case string. 275 | /// Example: "TheseAreFourWords" => "These Are Four Words". 276 | /// 277 | /// The string to format. 278 | /// A formatted string. 279 | private static string CamelCaseToTitleCase(string camelCaseString) 280 | { 281 | string formatted = string.Empty; 282 | 283 | if (!string.IsNullOrEmpty(camelCaseString)) 284 | { 285 | int lastStartIndex = 0; 286 | TextInfo textInfo = new CultureInfo("en-US", false).TextInfo; 287 | 288 | for (int i = 1; i < camelCaseString.Length; ++i) 289 | { 290 | bool isLastChar = (i == (camelCaseString.Length - 1)); 291 | 292 | if (char.IsUpper(camelCaseString[i])) 293 | { 294 | formatted += textInfo.ToTitleCase(camelCaseString.Substring(lastStartIndex, (i - lastStartIndex))) + " "; 295 | lastStartIndex = i; 296 | 297 | if (isLastChar) 298 | { 299 | // Last char is upper - given string could be e.g. "SlaveI" - so simply add the last char 300 | formatted += camelCaseString[i]; 301 | } 302 | } 303 | else if (isLastChar) 304 | { 305 | formatted += textInfo.ToTitleCase(camelCaseString.Substring(lastStartIndex, (i - lastStartIndex + 1))); 306 | } 307 | } 308 | } 309 | 310 | return formatted.Trim(); 311 | } 312 | 313 | /// 314 | /// Helper method for displaying selected value(s) as string. 315 | /// 316 | /// 317 | /// 318 | private static string ValueToString(object value) 319 | { 320 | if (value is List) 321 | { 322 | string valuesAsString = string.Empty; 323 | List values = (value as List); 324 | 325 | for (int i = 0; i < values.Count; ++i) 326 | { 327 | valuesAsString += values[i].ToString(); 328 | 329 | if (i < values.Count - 1) 330 | { 331 | valuesAsString += ", "; 332 | } 333 | } 334 | 335 | return valuesAsString; 336 | } 337 | 338 | return value.ToString(); 339 | } 340 | } 341 | } -------------------------------------------------------------------------------- /DynamicFormFlowSample/Global.asax: -------------------------------------------------------------------------------- 1 | <%@ Application Codebehind="Global.asax.cs" Inherits="DynamicFormFlowSample.WebApiApplication" Language="C#" %> 2 | -------------------------------------------------------------------------------- /DynamicFormFlowSample/Global.asax.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Web; 5 | using System.Web.Http; 6 | using System.Web.Routing; 7 | 8 | namespace DynamicFormFlowSample 9 | { 10 | public class WebApiApplication : System.Web.HttpApplication 11 | { 12 | protected void Application_Start() 13 | { 14 | GlobalConfiguration.Configure(WebApiConfig.Register); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /DynamicFormFlowSample/Models/Spaceship.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Bot.Builder.FormFlow; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace DynamicFormFlowSample.Models 6 | { 7 | [Serializable] 8 | public class Spaceship 9 | { 10 | public enum Sizes 11 | { 12 | NotDefined, 13 | Small, 14 | Mid, 15 | Large 16 | } 17 | 18 | public enum EngineTypes 19 | { 20 | NotDefined, 21 | Inpulse, 22 | Warp, 23 | Sublight, 24 | Hyper, 25 | TurboThrust, 26 | FTL 27 | } 28 | 29 | public enum WeaponTypes 30 | { 31 | NotDefined, 32 | None, 33 | Phaser, 34 | Disruptor, 35 | PhotonTorpedos, 36 | Laser, 37 | Ion, 38 | ProtonTorpedos, 39 | KineticEnergy, 40 | Batteries, 41 | Missiles 42 | } 43 | 44 | public enum CrewTypes 45 | { 46 | NotDefined, 47 | GoodGuys, 48 | BadGuys 49 | } 50 | 51 | public List Engines 52 | { 53 | get; 54 | set; 55 | } 56 | 57 | public List Weapons 58 | { 59 | get; 60 | set; 61 | } 62 | 63 | [Prompt("What size is the ship? {||}")] 64 | public Sizes Size 65 | { 66 | get; 67 | set; 68 | } 69 | 70 | /// 71 | /// Additional property added to allow the selection of just one weapon 72 | /// (instead of allowing multiple). 73 | /// 74 | public WeaponTypes Weapon 75 | { 76 | get 77 | { 78 | if (Weapons.Count > 0) 79 | { 80 | return Weapons[0]; 81 | } 82 | 83 | return WeaponTypes.NotDefined; 84 | } 85 | set 86 | { 87 | AddWeapon(value); 88 | } 89 | } 90 | 91 | [Optional] // Optional will add "No preference" as an option 92 | public CrewTypes Crew 93 | { 94 | get; 95 | set; 96 | } 97 | 98 | [Optional] 99 | public string Name 100 | { 101 | get; 102 | set; 103 | } 104 | 105 | public string ImageUri 106 | { 107 | get; 108 | set; 109 | } 110 | 111 | public Spaceship() 112 | { 113 | Reset(); 114 | } 115 | 116 | public Spaceship(Spaceship other) 117 | { 118 | Reset(); 119 | 120 | if (other != null) 121 | { 122 | Size = other.Size; 123 | Engines = other.Engines; 124 | Weapons = other.Weapons; 125 | Crew = other.Crew; 126 | Name = other.Name; 127 | } 128 | } 129 | 130 | public void AddEngine(EngineTypes engineType) 131 | { 132 | if (!Engines.Contains(engineType)) 133 | { 134 | Engines.Add(engineType); 135 | } 136 | } 137 | 138 | public void AddWeapon(WeaponTypes weaponType) 139 | { 140 | if (!Weapons.Contains(weaponType)) 141 | { 142 | Weapons.Add(weaponType); 143 | } 144 | } 145 | 146 | /// 147 | /// Tries to cast the given value to its proper type and verify its value based on 148 | /// the given property name. 149 | /// 150 | /// The value to cast. 151 | /// The name of the property. 152 | /// The value with a confirmed type and value (although return as object) 153 | /// or null in case of a failure. 154 | public static object VerifyPropertyValue(object propertyValue, string propertyName) 155 | { 156 | if (propertyValue is IList) 157 | { 158 | // We do not handle lists here 159 | return propertyValue; 160 | } 161 | 162 | object verifiedValue = null; 163 | string propertyValueAsString = (propertyValue != null) ? propertyValue.ToString() : "NotDefined"; 164 | 165 | try 166 | { 167 | switch (propertyName) 168 | { 169 | case nameof(Size): 170 | verifiedValue = (Sizes)Enum.Parse(typeof(Sizes), propertyValueAsString, true); 171 | break; 172 | case nameof(Engines): 173 | verifiedValue = (EngineTypes)Enum.Parse(typeof(EngineTypes), propertyValueAsString, true); 174 | break; 175 | case nameof(Weapons): 176 | verifiedValue = (WeaponTypes)Enum.Parse(typeof(WeaponTypes), propertyValueAsString, true); 177 | break; 178 | case nameof(Crew): 179 | verifiedValue = (CrewTypes)Enum.Parse(typeof(CrewTypes), propertyValueAsString, true); 180 | break; 181 | case nameof(Name): 182 | verifiedValue = propertyValueAsString; 183 | break; 184 | } 185 | } 186 | catch (ArgumentException e) 187 | { 188 | System.Diagnostics.Debug.WriteLine($"Failed to the property value: {e.Message}"); 189 | verifiedValue = null; 190 | } 191 | catch (OverflowException e) 192 | { 193 | System.Diagnostics.Debug.WriteLine($"Failed to the property value: {e.Message}"); 194 | verifiedValue = null; 195 | } 196 | 197 | return verifiedValue; 198 | } 199 | 200 | /// 201 | /// Sets the given value to the property (with the given name) of this instance. 202 | /// 203 | /// The value to set. 204 | /// The name of the property whose value to set. 205 | /// True, if the value was set successfully. False otherwise. 206 | public bool SetPropertyValue(object propertyValue, string propertyName) 207 | { 208 | bool isList = (propertyValue is IList); 209 | object verifiedValue = VerifyPropertyValue(propertyValue, propertyName); 210 | 211 | if (verifiedValue != null) 212 | { 213 | switch (propertyName) 214 | { 215 | case nameof(Size): 216 | Size = (Sizes)verifiedValue; 217 | break; 218 | case nameof(Engines): 219 | if (isList) 220 | { 221 | foreach (object value in (propertyValue as IList)) 222 | { 223 | try 224 | { 225 | AddEngine((EngineTypes)value); 226 | } 227 | catch (InvalidCastException e) 228 | { 229 | System.Diagnostics.Debug.WriteLine("Failed to add engine: " + e.Message); 230 | } 231 | } 232 | } 233 | else 234 | { 235 | AddEngine((EngineTypes)verifiedValue); 236 | } 237 | 238 | break; 239 | case nameof(Weapons): 240 | if (isList) 241 | { 242 | foreach (object value in (propertyValue as IList)) 243 | { 244 | try 245 | { 246 | AddWeapon((WeaponTypes)value); 247 | } 248 | catch (InvalidCastException e) 249 | { 250 | System.Diagnostics.Debug.WriteLine("Failed to add weapon: " + e.Message); 251 | } 252 | } 253 | } 254 | else 255 | { 256 | AddWeapon((WeaponTypes)verifiedValue); 257 | } 258 | 259 | break; 260 | case nameof(Crew): 261 | Crew = (CrewTypes)verifiedValue; 262 | break; 263 | case nameof(Name): 264 | Name = (string)verifiedValue; 265 | break; 266 | } 267 | 268 | } 269 | 270 | return (verifiedValue != null); 271 | } 272 | 273 | /// 274 | /// Clears the value of the given property. 275 | /// 276 | /// The name of the property whose value to clear. 277 | public void ClearPropertyValue(string propertyName) 278 | { 279 | switch (propertyName) 280 | { 281 | case nameof(Size): 282 | Size = Sizes.NotDefined; 283 | break; 284 | case nameof(Engines): 285 | Engines.Clear(); 286 | break; 287 | case nameof(Weapons): 288 | Weapons.Clear(); 289 | break; 290 | case nameof(Crew): 291 | Crew = CrewTypes.NotDefined; 292 | break; 293 | case nameof(Name): 294 | Name = string.Empty; 295 | break; 296 | } 297 | } 298 | 299 | /// 300 | /// Checks if the given spaceship is partial match to this one. 301 | /// Any differences (other than properties not defined) will be considered mismatch. 302 | /// 303 | /// The spaceship to compare to this one. 304 | /// True, if the spaceships are a partial match. False otherwise. 305 | public bool IsPartialMatch(Spaceship other) 306 | { 307 | if (other == null) 308 | { 309 | return false; 310 | } 311 | 312 | if (other.Size != Sizes.NotDefined && other.Size != Size) 313 | { 314 | return false; 315 | } 316 | 317 | foreach (EngineTypes engineType in other.Engines) 318 | { 319 | if (!Engines.Contains(engineType)) 320 | { 321 | return false; 322 | } 323 | } 324 | 325 | foreach (WeaponTypes weaponType in other.Weapons) 326 | { 327 | if (!Weapons.Contains(weaponType)) 328 | { 329 | return false; 330 | } 331 | } 332 | 333 | if (other.Crew != CrewTypes.NotDefined && other.Crew != Crew) 334 | { 335 | return false; 336 | } 337 | 338 | return true; 339 | } 340 | 341 | public string PropertiesAsFormattedString() 342 | { 343 | string retval = $"Size: {Size.ToString()}\r\nEngines: "; 344 | 345 | if (Engines.Count == 0) 346 | { 347 | retval += "N/A"; 348 | } 349 | else 350 | { 351 | for (int i = 0; i < Engines.Count; ++i) 352 | { 353 | retval += Engines[i].ToString(); 354 | 355 | if (i < Engines.Count - 1) 356 | { 357 | retval += ", "; 358 | } 359 | } 360 | } 361 | 362 | retval += "\r\nWeapons: "; 363 | 364 | if (Weapons.Count == 0) 365 | { 366 | retval += "N/A"; 367 | } 368 | else 369 | { 370 | for (int i = 0; i < Weapons.Count; ++i) 371 | { 372 | retval += Weapons[i].ToString(); 373 | 374 | if (i < Weapons.Count - 1) 375 | { 376 | retval += ", "; 377 | } 378 | } 379 | } 380 | 381 | retval += $"\r\nCrew: {Crew.ToString()}"; 382 | return retval; 383 | } 384 | 385 | public override string ToString() 386 | { 387 | string retval = "[\"" + Name + "\"; " + Size + "; "; 388 | 389 | for (int i = 0; i < Engines.Count; ++i) 390 | { 391 | if (i == 0) 392 | { 393 | retval += "["; 394 | } 395 | 396 | retval += Engines[i].ToString(); 397 | 398 | if (i < Engines.Count - 1) 399 | { 400 | retval += "; "; 401 | } 402 | else 403 | { 404 | retval += "]; "; 405 | } 406 | } 407 | 408 | for (int i = 0; i < Weapons.Count; ++i) 409 | { 410 | if (i == 0) 411 | { 412 | retval += "["; 413 | } 414 | 415 | retval += Weapons[i].ToString(); 416 | 417 | if (i < Weapons.Count - 1) 418 | { 419 | retval += "; "; 420 | } 421 | else 422 | { 423 | retval += "]; "; 424 | } 425 | } 426 | 427 | retval += Crew + "]"; 428 | return retval; 429 | } 430 | 431 | private void Reset() 432 | { 433 | Size = Sizes.NotDefined; 434 | Engines = new List(); 435 | Weapons = new List(); 436 | Crew = CrewTypes.NotDefined; 437 | Name = string.Empty; 438 | } 439 | } 440 | } -------------------------------------------------------------------------------- /DynamicFormFlowSample/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("DynamicFormFlowSample")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("DynamicFormFlowSample")] 13 | [assembly: AssemblyCopyright("Copyright © 2016")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("a8ba1066-5695-4d71-abb4-65e5a5e0c3d4")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Revision and Build Numbers 33 | // by using the '*' as shown below: 34 | [assembly: AssemblyVersion("1.0.0.0")] 35 | [assembly: AssemblyFileVersion("1.0.0.0")] 36 | -------------------------------------------------------------------------------- /DynamicFormFlowSample/Web.Debug.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 17 | 18 | 29 | 30 | -------------------------------------------------------------------------------- /DynamicFormFlowSample/Web.Release.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 17 | 18 | 19 | 30 | 31 | -------------------------------------------------------------------------------- /DynamicFormFlowSample/Web.config: -------------------------------------------------------------------------------- 1 |  2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /DynamicFormFlowSample/default.htm: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 |

DynamicFormFlowSample

9 |

Describe your bot here and your terms of use etc.

10 |

Visit Bot Framework to register your bot. When you register it, remember to set your bot's endpoint to

https://your_bots_hostname/api/messages

11 | 12 | 13 | -------------------------------------------------------------------------------- /DynamicFormFlowSample/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Tomi Paananen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dynamic FormFlow Sample # 2 | 3 | This sample demonstrates how to add dynamic features to FormFlow implementation 4 | of a bot built on Microsoft Bot Framework. The central points are explained in 5 | my blog post creatively named 6 | [How to Create Dynamic FormFlow](http://tomipaananen.azurewebsites.net/?p=1641). 7 | 8 | The approach implemented by this sample is especially useful for scenarios where 9 | the user is searching an item from an existing catalog. Instead of displaying 10 | all the options for narrowing down the desired item, we only display options 11 | available based on the data previously collected. In some cases we can skip part 12 | of the queries altogether as they have but only one option. See the three 13 | example flows below. 14 | 15 | **Few examples of the flow:** 16 | 17 | | Flow 1 | Flow 2 | Flow 3 | 18 | | ------ | ------ | ------ | 19 | | ![Flow 1](Documentation/Screenshots/Scenario1.png?raw=true) | ![Flow 2](Documentation/Screenshots/Scenario2.png?raw=true) | ![Flow3](Documentation/Screenshots/Scenario3.png?raw=true) | 20 | 21 | 22 | ## Important classes ## 23 | 24 | **[Spaceship](/DynamicFormFlowSample/Models/Spaceship.cs)** represents the item 25 | we are searching for and its properties. This is the class the FormFlow takes 26 | and starts filling values with. 27 | 28 | **[SpaceshipData](/DynamicFormFlowSample/Data/SpaceshipData.cs)** contains the 29 | catalog and some helper methods. 30 | 31 | **[SpaceshipSelectionForm](/DynamicFormFlowSample/Forms/SpaceshipSelectionForm.cs)** 32 | contains the FormFlow builder and the methods implementing the dynamics: 33 | checking if the fields (queries) are necessary, what options should be available 34 | and response validation. 35 | 36 | **[SpaceshipSelectionDialog](/DynamicFormFlowSample/Dialogs/SpaceshipSelectionDialog.cs)** 37 | is the root dialog of the bot and takes control of the flow once the FormFlow 38 | is finished. If more than one option remains, it will display a carousel of the 39 | spaceships left for the user to choose from. 40 | 41 | 42 | ## See also ## 43 | 44 | * [FormFlow documentation](https://docs.botframework.com/en-us/csharp/builder/sdkreference/forms.html) 45 | * [Pizza Bot Sample](https://github.com/Microsoft/BotBuilder/tree/master/CSharp/Samples/PizzaBot) 46 | --------------------------------------------------------------------------------