diff --git a/CSharp/cards-AdaptiveCards/AdaptiveCards.csproj b/CSharp/cards-AdaptiveCards/AdaptiveCards.csproj new file mode 100644 index 0000000000..9ae867218a --- /dev/null +++ b/CSharp/cards-AdaptiveCards/AdaptiveCards.csproj @@ -0,0 +1,178 @@ + + + + + Debug + AnyCPU + + + 2.0 + {A8BA1066-5695-4D71-ABB4-65E5A5E0C3D4} + {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} + Library + Properties + BotBuilder.Samples.AdaptiveCards + BotBuilder.Samples.AdaptiveCards + v4.6 + true + + + + + + + + + + + true + full + false + bin\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\ + TRACE + prompt + 4 + + + + packages\Microsoft.AdaptiveCards.0.5.0\lib\net452\AdaptiveCards.dll + + + packages\Autofac.4.6.0\lib\net45\Autofac.dll + + + packages\Chronic.Signed.0.3.2\lib\net40\Chronic.dll + True + + + packages\Microsoft.Bot.Builder.3.8.1.0\lib\net46\Microsoft.Bot.Builder.dll + + + packages\Microsoft.Bot.Builder.3.8.1.0\lib\net46\Microsoft.Bot.Builder.Autofac.dll + + + packages\Microsoft.Bot.Builder.3.8.1.0\lib\net46\Microsoft.Bot.Connector.dll + + + + packages\Microsoft.IdentityModel.Protocol.Extensions.1.0.4.403061554\lib\net45\Microsoft.IdentityModel.Protocol.Extensions.dll + + + packages\Microsoft.Rest.ClientRuntime.2.3.8\lib\net452\Microsoft.Rest.ClientRuntime.dll + + + packages\Microsoft.WindowsAzure.ConfigurationManager.3.2.3\lib\net40\Microsoft.WindowsAzure.Configuration.dll + True + + + packages\Newtonsoft.Json.10.0.2\lib\net45\Newtonsoft.Json.dll + + + + packages\System.IdentityModel.Tokens.Jwt.4.0.4.403061554\lib\net45\System.IdentityModel.Tokens.Jwt.dll + + + + + packages\Microsoft.AspNet.WebApi.Client.5.2.3\lib\net45\System.Net.Http.Formatting.dll + True + + + + + + + + + + + + + + packages\Microsoft.AspNet.WebApi.Core.5.2.3\lib\net45\System.Web.Http.dll + True + + + packages\Microsoft.AspNet.WebApi.WebHost.5.2.3\lib\net45\System.Web.Http.WebHost.dll + True + + + + + + + + + + + + + Designer + + + + + + + + + + Global.asax + + + + + + + + + Web.config + + + Web.config + + + + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + true + + + + + + + + + True + True + 3979 + / + http://localhost:3979/ + False + False + + + False + + + + + + \ No newline at end of file diff --git a/CSharp/cards-AdaptiveCards/AdaptiveCards.sln b/CSharp/cards-AdaptiveCards/AdaptiveCards.sln new file mode 100644 index 0000000000..371778d3f0 --- /dev/null +++ b/CSharp/cards-AdaptiveCards/AdaptiveCards.sln @@ -0,0 +1,27 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26430.6 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdaptiveCards", "AdaptiveCards.csproj", "{A8BA1066-5695-4D71-ABB4-65E5A5E0C3D4}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "README", "README", "{09798947-D0A9-49B0-8D28-1CB803484712}" + ProjectSection(SolutionItems) = preProject + README.md = README.md + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A8BA1066-5695-4D71-ABB4-65E5A5E0C3D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A8BA1066-5695-4D71-ABB4-65E5A5E0C3D4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A8BA1066-5695-4D71-ABB4-65E5A5E0C3D4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A8BA1066-5695-4D71-ABB4-65E5A5E0C3D4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/CSharp/cards-AdaptiveCards/App_Start/WebApiConfig.cs b/CSharp/cards-AdaptiveCards/App_Start/WebApiConfig.cs new file mode 100644 index 0000000000..739f95c88c --- /dev/null +++ b/CSharp/cards-AdaptiveCards/App_Start/WebApiConfig.cs @@ -0,0 +1,33 @@ +namespace BotBuilder.Samples.AdaptiveCards +{ + using System.Web.Http; + using Newtonsoft.Json; + using Newtonsoft.Json.Serialization; + + public static class WebApiConfig + { + public static void Register(HttpConfiguration config) + { + // Json settings + config.Formatters.JsonFormatter.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; + config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); + config.Formatters.JsonFormatter.SerializerSettings.Formatting = Formatting.Indented; + JsonConvert.DefaultSettings = () => new JsonSerializerSettings() + { + ContractResolver = new CamelCasePropertyNamesContractResolver(), + Formatting = Newtonsoft.Json.Formatting.Indented, + NullValueHandling = NullValueHandling.Ignore, + }; + + // Web API configuration and services + + // Web API routes + config.MapHttpAttributeRoutes(); + + config.Routes.MapHttpRoute( + name: "DefaultApi", + routeTemplate: "api/{controller}/{id}", + defaults: new { id = RouteParameter.Optional }); + } + } +} diff --git a/CSharp/cards-AdaptiveCards/Controllers/MessagesController.cs b/CSharp/cards-AdaptiveCards/Controllers/MessagesController.cs new file mode 100644 index 0000000000..ec3f16d86d --- /dev/null +++ b/CSharp/cards-AdaptiveCards/Controllers/MessagesController.cs @@ -0,0 +1,61 @@ +namespace BotBuilder.Samples.AdaptiveCards +{ + using System.Net; + using System.Net.Http; + using System.Threading.Tasks; + using System.Web.Http; + using Microsoft.Bot.Builder.Dialogs; + using Microsoft.Bot.Connector; + + [BotAuthentication] + public class MessagesController : ApiController + { + /// + /// POST: api/Messages + /// Receive a message from a user and reply to it + /// + public async Task Post([FromBody]Activity activity) + { + if (activity.Type == ActivityTypes.Message) + { + await Conversation.SendAsync(activity, () => new RootDialog()); + } + else + { + this.HandleSystemMessage(activity); + } + + var response = Request.CreateResponse(HttpStatusCode.OK); + return response; + } + + private Activity HandleSystemMessage(Activity message) + { + if (message.Type == ActivityTypes.DeleteUserData) + { + // Implement user deletion here + // If we handle user deletion, return a real message + } + else if (message.Type == ActivityTypes.ConversationUpdate) + { + // Handle conversation state changes, like members being added and removed + // Use Activity.MembersAdded and Activity.MembersRemoved and Activity.Action for info + // Not available in all channels + } + else if (message.Type == ActivityTypes.ContactRelationUpdate) + { + // Handle add/remove from contact lists + // Activity.From + Activity.Action represent what happened + } + else if (message.Type == ActivityTypes.Typing) + { + // Handle knowing tha the user is typing + } + else if (message.Type == ActivityTypes.Ping) + { + } + + return null; + } + } +} \ No newline at end of file diff --git a/CSharp/cards-AdaptiveCards/Dialogs/HotelsDialog.cs b/CSharp/cards-AdaptiveCards/Dialogs/HotelsDialog.cs new file mode 100644 index 0000000000..f4c63eec25 --- /dev/null +++ b/CSharp/cards-AdaptiveCards/Dialogs/HotelsDialog.cs @@ -0,0 +1,143 @@ +namespace BotBuilder.Samples.AdaptiveCards +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using global::AdaptiveCards; + using Microsoft.Bot.Builder.Dialogs; + using Microsoft.Bot.Builder.FormFlow; + using Microsoft.Bot.Connector; + using Newtonsoft.Json.Linq; + + [Serializable] + public class HotelsDialog : IDialog + { + public async Task StartAsync(IDialogContext context) + { + var message = context.Activity as IMessageActivity; + var query = HotelsQuery.Parse(message.Value); + + await context.PostAsync($"Ok. Searching for Hotels in {query.Destination} from {query.Checkin.Value.ToString("MM/dd")} to {query.Checkin.Value.AddDays(query.Nights.Value).ToString("MM/dd")}..."); + + try + { + await SearchHotels(context, query); + } + catch (FormCanceledException ex) + { + await context.PostAsync($"Oops! Something went wrong :( Technical Details: {ex.InnerException.Message}"); + } + } + + private async Task SearchHotels(IDialogContext context, HotelsQuery searchQuery) + { + var hotels = this.GetHotels(searchQuery); + + // Result count + var title = $"I found in total {hotels.Count()} hotels for your dates:"; + var intro = new List() + { + new TextBlock() + { + Text = title, + Size = TextSize.ExtraLarge, + Speak = $"{title}" + } + }; + + // Hotels in rows of three + var rows = Split(hotels, 3) + .Select(group => new ColumnSet() + { + Columns = new List(group.Select(AsHotelItem)) + }); + + var card = new AdaptiveCard() + { + Body = intro.Union(rows).ToList() + }; + + Attachment attachment = new Attachment() + { + ContentType = AdaptiveCard.ContentType, + Content = card + }; + + var reply = context.MakeMessage(); + reply.Attachments.Add(attachment); + + await context.PostAsync(reply); + } + + private Column AsHotelItem(Hotel hotel) + { + var submitActionData = JObject.Parse("{ \"Type\": \"HotelSelection\" }"); + submitActionData.Merge(JObject.FromObject(hotel)); + + return new Column() + { + Size = "20", + Items = new List() + { + new TextBlock() + { + Text = hotel.Name, + Speak = $"{hotel.Name}", + HorizontalAlignment = HorizontalAlignment.Center, + Wrap = false, + Weight = TextWeight.Bolder + }, + new Image() + { + Size = ImageSize.Auto, + Url = hotel.Image + } + }, + SelectAction = new SubmitAction() + { + DataJson = submitActionData.ToString() + } + }; + } + + private IEnumerable GetHotels(HotelsQuery searchQuery) + { + var hotels = new List(); + + // Filling the hotels results manually just for demo purposes + for (int i = 1; i <= 6; i++) + { + var random = new Random(i); + Hotel hotel = new Hotel() + { + Name = $"Hotel {i}", + Location = searchQuery.Destination, + Rating = random.Next(1, 5), + NumberOfReviews = random.Next(0, 5000), + PriceStarting = random.Next(80, 450), + Image = $"https://placeholdit.imgix.net/~text?txtsize=35&txt=Hotel+{i}&w=500&h=260", + MoreImages = new List() + { + "https://placeholdit.imgix.net/~text?txtsize=65&txt=Pic+1&w=450&h=300", + "https://placeholdit.imgix.net/~text?txtsize=65&txt=Pic+2&w=450&h=300", + "https://placeholdit.imgix.net/~text?txtsize=65&txt=Pic+3&w=450&h=300", + "https://placeholdit.imgix.net/~text?txtsize=65&txt=Pic+4&w=450&h=300" + } + }; + + hotels.Add(hotel); + } + + hotels.Sort((h1, h2) => h1.PriceStarting.CompareTo(h2.PriceStarting)); + + return hotels; + } + public static IEnumerable> Split(IEnumerable list, int parts) + { + return list.Select((item, ix) => new { ix, item }) + .GroupBy(x => x.ix % parts) + .Select(x => x.Select(y => y.item)); + } + } +} \ No newline at end of file diff --git a/CSharp/cards-AdaptiveCards/Dialogs/RootDialog.cs b/CSharp/cards-AdaptiveCards/Dialogs/RootDialog.cs new file mode 100644 index 0000000000..d01f6c8051 --- /dev/null +++ b/CSharp/cards-AdaptiveCards/Dialogs/RootDialog.cs @@ -0,0 +1,295 @@ +namespace BotBuilder.Samples.AdaptiveCards +{ + using System; + using System.Collections.Generic; + using System.ComponentModel.DataAnnotations; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using global::AdaptiveCards; + using Microsoft.Bot.Builder.Dialogs; + using Microsoft.Bot.Connector; + using Newtonsoft.Json; + + [Serializable] + public class RootDialog : IDialog + { + private const string FlightsOption = "Flights"; + + private const string HotelsOption = "Hotels"; + + public async Task StartAsync(IDialogContext context) + { + context.Wait(this.MessageReceivedAsync); + } + + public virtual async Task MessageReceivedAsync(IDialogContext context, IAwaitable result) + { + var message = await result; + + if (message.Value != null) + { + // Got an Action Submit + dynamic value = message.Value; + string submitType = value.Type.ToString(); + switch (submitType) + { + case "HotelSearch": + HotelsQuery query; + try + { + query = HotelsQuery.Parse(value); + + // Trigger validation using Data Annotations attributes from the HotelsQuery model + List results = new List(); + bool valid = Validator.TryValidateObject(query, new ValidationContext(query, null, null), results, true); + if (!valid) + { + // Some field in the Hotel Query are not valid + var errors = string.Join("\n", results.Select(o => " - " + o.ErrorMessage)); + await context.PostAsync("Please complete all the search parameters:\n" + errors); + return; + } + } + catch (InvalidCastException) + { + // Hotel Query could not be parsed + await context.PostAsync("Please complete all the search parameters"); + return; + } + + // Proceed with hotels search + await context.Forward(new HotelsDialog(), this.ResumeAfterOptionDialog, message, CancellationToken.None); + + return; + + case "HotelSelection": + await SendHotelSelectionAsync(context, (Hotel)JsonConvert.DeserializeObject(value.ToString())); + context.Wait(MessageReceivedAsync); + + return; + } + } + + if (message.Text != null && (message.Text.ToLower().Contains("help") || message.Text.ToLower().Contains("support") || message.Text.ToLower().Contains("problem"))) + { + await context.Forward(new SupportDialog(), this.ResumeAfterSupportDialog, message, CancellationToken.None); + } + else + { + await ShowOptionsAsync(context); + } + } + + private async Task ShowOptionsAsync(IDialogContext context) + { + AdaptiveCard card = new AdaptiveCard() + { + Body = new List() + { + new Container() + { + Speak = "Hello!Are you looking for a flight or a hotel?", + Items = new List() + { + new ColumnSet() + { + Columns = new List() + { + new Column() + { + Size = ColumnSize.Auto, + Items = new List() + { + new Image() + { + Url = "https://placeholdit.imgix.net/~text?txtsize=65&txt=Adaptive+Cards&w=300&h=300", + Size = ImageSize.Medium, + Style = ImageStyle.Person + } + } + }, + new Column() + { + Size = ColumnSize.Stretch, + Items = new List() + { + new TextBlock() + { + Text = "Hello!", + Weight = TextWeight.Bolder, + IsSubtle = true + }, + new TextBlock() + { + Text = "Are you looking for a flight or a hotel?", + Wrap = true + } + } + } + } + } + } + } + }, + // Buttons + Actions = new List() { + new ShowCardAction() + { + Title = "Hotels", + Speak = "Hotels", + Card = GetHotelSearchCard() + }, + new ShowCardAction() + { + Title = "Flights", + Speak = "Flights", + Card = new AdaptiveCard() + { + Body = new List() + { + new TextBlock() + { + Text = "Flights is not implemented =(", + Speak = "Flights is not implemented", + Weight = TextWeight.Bolder + } + } + } + } + } + }; + + Attachment attachment = new Attachment() + { + ContentType = AdaptiveCard.ContentType, + Content = card + }; + + var reply = context.MakeMessage(); + reply.Attachments.Add(attachment); + + await context.PostAsync(reply, CancellationToken.None); + + context.Wait(MessageReceivedAsync); + } + private async Task ResumeAfterOptionDialog(IDialogContext context, IAwaitable result) + { + context.Wait(this.MessageReceivedAsync); + } + + private async Task ResumeAfterSupportDialog(IDialogContext context, IAwaitable result) + { + var ticketNumber = await result; + + await context.PostAsync($"Thanks for contacting our support team. Your ticket number is {ticketNumber}."); + context.Wait(this.MessageReceivedAsync); + } + + private static AdaptiveCard GetHotelSearchCard() + { + return new AdaptiveCard() + { + Body = new List() + { + // Hotels Search form + new TextBlock() + { + Text = "Welcome to the Hotels finder!", + Speak = "Welcome to the Hotels finder!", + Weight = TextWeight.Bolder, + Size = TextSize.Large + }, + new TextBlock() { Text = "Please enter your destination:" }, + new TextInput() + { + Id = "Destination", + Speak = "Please enter your destination", + Placeholder = "Miami, Florida", + Style = TextInputStyle.Text + }, + new TextBlock() { Text = "When do you want to check in?" }, + new DateInput() + { + Id = "Checkin", + Speak = "When do you want to check in?" + }, + new TextBlock() { Text = "How many nights do you want to stay?" }, + new NumberInput() + { + Id = "Nights", + Min = 1, + Max = 60, + Speak = "How many nights do you want to stay?" + } + }, + Actions = new List() + { + new SubmitAction() + { + Title = "Search", + Speak = "Search", + DataJson = "{ \"Type\": \"HotelSearch\" }" + } + } + }; + } + + private static async Task SendHotelSelectionAsync(IDialogContext context, Hotel hotel) + { + var description = $"{hotel.Rating} start with {hotel.NumberOfReviews}. From ${hotel.PriceStarting} per night."; + var card = new AdaptiveCard() + { + Body = new List() + { + new Container() + { + Items = new List() + { + new TextBlock() + { + Text = $"{hotel.Name} in {hotel.Location}", + Weight = TextWeight.Bolder, + Speak = $"{hotel.Name}" + }, + new TextBlock() + { + Text = description, + Speak = $"{description}" + }, + new Image() + { + Size = ImageSize.Large, + Url = hotel.Image + }, + new ImageSet() + { + ImageSize = ImageSize.Medium, + Separation = SeparationStyle.Strong, + Images = hotel.MoreImages.Select(img => new Image() + { + Url = img + }).ToList() + } + }, + SelectAction = new OpenUrlAction() + { + Url = "https://dev.botframework.com/" + } + } + } + }; + + Attachment attachment = new Attachment() + { + ContentType = AdaptiveCard.ContentType, + Content = card + }; + + var reply = context.MakeMessage(); + reply.Attachments.Add(attachment); + + await context.PostAsync(reply, CancellationToken.None); + } + } +} \ No newline at end of file diff --git a/CSharp/cards-AdaptiveCards/Dialogs/SupportDialog.cs b/CSharp/cards-AdaptiveCards/Dialogs/SupportDialog.cs new file mode 100644 index 0000000000..0e80da5f6c --- /dev/null +++ b/CSharp/cards-AdaptiveCards/Dialogs/SupportDialog.cs @@ -0,0 +1,27 @@ +namespace BotBuilder.Samples.AdaptiveCards +{ + using System; + using System.Threading.Tasks; + using Microsoft.Bot.Builder.Dialogs; + using Microsoft.Bot.Connector; + + [Serializable] + public class SupportDialog : IDialog + { + public async Task StartAsync(IDialogContext context) + { + context.Wait(this.MessageReceivedAsync); + } + + public virtual async Task MessageReceivedAsync(IDialogContext context, IAwaitable result) + { + var message = await result; + + var ticketNumber = new Random().Next(0, 20000); + + await context.PostAsync($"Your message '{message.Text}' was registered. Once we resolve it; we will get back to you."); + + context.Done(ticketNumber); + } + } +} diff --git a/CSharp/cards-AdaptiveCards/Global.asax b/CSharp/cards-AdaptiveCards/Global.asax new file mode 100644 index 0000000000..0cc9faaeb3 --- /dev/null +++ b/CSharp/cards-AdaptiveCards/Global.asax @@ -0,0 +1 @@ +<%@ Application Codebehind="Global.asax.cs" Inherits="BotBuilder.Samples.AdaptiveCards.WebApiApplication" Language="C#" %> diff --git a/CSharp/cards-AdaptiveCards/Global.asax.cs b/CSharp/cards-AdaptiveCards/Global.asax.cs new file mode 100644 index 0000000000..3169d4a341 --- /dev/null +++ b/CSharp/cards-AdaptiveCards/Global.asax.cs @@ -0,0 +1,12 @@ +namespace BotBuilder.Samples.AdaptiveCards +{ + using System.Web.Http; + + public class WebApiApplication : System.Web.HttpApplication + { + protected void Application_Start() + { + GlobalConfiguration.Configure(WebApiConfig.Register); + } + } +} diff --git a/CSharp/cards-AdaptiveCards/Hotel.cs b/CSharp/cards-AdaptiveCards/Hotel.cs new file mode 100644 index 0000000000..98cd78470c --- /dev/null +++ b/CSharp/cards-AdaptiveCards/Hotel.cs @@ -0,0 +1,23 @@ +namespace BotBuilder.Samples.AdaptiveCards +{ + using System; + using System.Collections.Generic; + + [Serializable] + public class Hotel + { + public string Name { get; set; } + + public int Rating { get; set; } + + public int NumberOfReviews { get; set; } + + public int PriceStarting { get; set; } + + public string Image { get; set; } + + public IEnumerable MoreImages { get; set; } + + public string Location { get; set; } + } +} \ No newline at end of file diff --git a/CSharp/cards-AdaptiveCards/HotelsQuery.cs b/CSharp/cards-AdaptiveCards/HotelsQuery.cs new file mode 100644 index 0000000000..4b2a4aafd0 --- /dev/null +++ b/CSharp/cards-AdaptiveCards/HotelsQuery.cs @@ -0,0 +1,34 @@ +namespace BotBuilder.Samples.AdaptiveCards +{ + using System; + using System.ComponentModel.DataAnnotations; + + public class HotelsQuery + { + [Required] + public string Destination { get; set; } + + [Required] + public DateTime? Checkin { get; set; } + + [Range(1, 60)] + public int? Nights { get; set; } + + public static HotelsQuery Parse(dynamic o) + { + try + { + return new HotelsQuery + { + Destination = o.Destination.ToString(), + Checkin = DateTime.Parse(o.Checkin.ToString()), + Nights = int.Parse(o.Nights.ToString()) + }; + } + catch + { + throw new InvalidCastException("HotelQuery could not be read"); + } + } + } +} \ No newline at end of file diff --git a/CSharp/cards-AdaptiveCards/Properties/AssemblyInfo.cs b/CSharp/cards-AdaptiveCards/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..ff6e0ee3e1 --- /dev/null +++ b/CSharp/cards-AdaptiveCards/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("AdaptiveCards")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("AdaptiveCards")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("a8ba1066-5695-4d71-abb4-65e5a5e0c3d4")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Revision and Build Numbers +// by using the '*' as shown below: +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/CSharp/cards-AdaptiveCards/README.md b/CSharp/cards-AdaptiveCards/README.md new file mode 100644 index 0000000000..8a472237a2 --- /dev/null +++ b/CSharp/cards-AdaptiveCards/README.md @@ -0,0 +1,376 @@ +# Adaptive Cards Bot Sample + +A sample bot using [Adaptive Cards](http://adaptivecards.io/) and how to handle user interaction with them. + +[![Deploy to Azure][Deploy Button]][Deploy CSharp/AdaptiveCards] + +[Deploy Button]: https://azuredeploy.net/deploybutton.png +[Deploy CSharp/AdaptiveCards]: https://azuredeploy.net + +### Prerequisites + +The minimum prerequisites to run this sample are: +* The latest update of Visual Studio 2015. You can download the community version [here](http://www.visualstudio.com) for free. +* The Bot Framework Emulator. To install the Bot Framework Emulator, download it from [here](https://emulator.botframework.com/). Please refer to [this documentation article](https://github.com/microsoft/botframework-emulator/wiki/Getting-Started) to know more about the Bot Framework Emulator. + +### Code Highlights + +[Adaptive Cards](http://adaptivecards.io/) are an open card exchange format that enables developers to exchange UI content in a common and consistent way. The Bot Framework has the ability to use this type of cards and provide a richer interaction experience. + +The Adaptive Card can contain any combination of text, speech, images, buttons, and input fields. Adaptive Cards are created using the JSON format specified in Adaptive Cards schema, which gives you full control over card content and format. + +For .NET, there is [Microsoft.AdaptiveCards NuGet](https://www.nuget.org/packages/Microsoft.AdaptiveCards/) that implements classes for building these cards and handles the serialization, so you don't have to hustle with JSON and you get Intellisense from Visual Studio. + +The aesthetics of the card are adapted to the channel's look and feel, making it feel native to the app and familiar to the user. You can use the [Adaptive Cards' Visualizer](http://adaptivecards.io/visualizer) to see how your card renders on different channels. + +> Note: At the time of writing this sample, the Adaptive Cards support on the different channels is limited. This sample works properly on the Emulator and WebChat channel. See [more information](https://github.com/Microsoft/AdaptiveCards/issues/367) about channel support. + +See how the sample composes a [welcome card](Dialogs/RootDialog.cs#L86-L161) along with search options: + +````C# +AdaptiveCard card = new AdaptiveCard() +{ + Body = new List() + { + new Container() + { + Speak = "Hello!Are you looking for a flight or a hotel?", + Items = new List() + { + new ColumnSet() + { + Columns = new List() + { + new Column() + { + Size = ColumnSize.Auto, + Items = new List() + { + new Image() + { + Url = "https://placeholdit.imgix.net/~text?txtsize=65&txt=Adaptive+Cards&w=300&h=300", + Size = ImageSize.Medium, + Style = ImageStyle.Person + } + } + }, + new Column() + { + Size = ColumnSize.Stretch, + Items = new List() + { + new TextBlock() + { + Text = "Hello!", + Weight = TextWeight.Bolder, + IsSubtle = true + }, + new TextBlock() + { + Text = "Are you looking for a flight or a hotel?", + Wrap = true + } + } + } + } + } + } + } + }, + // Buttons + Actions = new List() { /* */ } +}; +```` + +The previous code will generate a card similar to this one: + +![Welcome Card](images/welcome-card.png) + +Adaptive Cards are created using JSON, but with the [Microsoft.AdaptiveCards NuGet](https://www.nuget.org/packages/Microsoft.AdaptiveCards/) you can create them by composing card objects. You then attach these AdaptiveCard objects to a message: + +````C# +Attachment attachment = new Attachment() +{ + ContentType = AdaptiveCard.ContentType, + Content = card +}; + +var reply = context.MakeMessage(); +reply.Attachments.Add(attachment); + +await context.PostAsync(reply, CancellationToken.None); +```` + +Adaptive Cards contain many elements that allow to exchange UI content in a common and consistent way. Some of these elements are: + +- **TextBlock** + + The TextBlock element allows for the inclusion of text, with various font sizes, weight and color. + +- **ImageSet** and **Image** + + The ImageSet allows for the inclusion of a collection images like a photo set, and the Image element allows for the inclusion of images. + +- **Input elements** + + Input elements allow you to ask for native UI to build simple forms: + + - **Input.Text** - get text content from the user + - **Input.Date** - get a Date from the user + - **Input.Time** - get a Time from the user + - **Input.Number** - get a Number from the user + - **Input.ChoiceSet** - Give the user a set of choices and have them pick + - **Input.ToggleChoice** - Give the user a single choice between two items and have them pick + +- **Container** + + A Container is a CardElement which contains a list of CardElements that are logically grouped. + +- **ColumnSet** and **Column** + + The columnSet element adds the ability to have a set of Column objects. + +- **FactSet** + + The FactSet element makes it simple to display a series of "facts" (e.g. name/value pairs) in a tabular form. + +Finally, Adaptive Cards support special elements that enable interaction: + +- **Action.OpenUrl** + + When Action.OpenUrl is invoked it will show the given url, either by launching it to an external web browser or showing in-situ with embedded web browser. + +- **Action.Submit** + + Action.Submit gathers up input fields, merges with optional data field and generates event to client asking for data to be submitted. The Bot Framework will send an activity through the messaging medium to the bot. + +- **Action.Http** + + Action.Http represents the properties needed to do an Http request. All input properties are available for use via data binding. Properties can be data bound to the Uri and Body properties, allowing you to send a request to an arbitrary url. + +- **Action.ShowCard** + + Action.ShowCard defines an inline AdaptiveCard which is shown to the user when it is clicked. + +You can visit the [Adaptive Cards Schema Explorer](http://adaptivecards.io/explorer/) for samples and the properties each element supports. + +#### Creating an inline Adaptive Card + +A card may offer the user multiple options to continue. Each option can be offered as a button that, once clicked, expands into a new card within the existing one. This is accomplised using a *ShowCard Action*. +See the [Flight's option](Dialogs/RootDialog.cs#L143-L159) for a simple card and the [Hotel's option](Dialogs/RootDialog.cs#L137-L142) for a complex one. +These are defined within the `Actions` element of the main card as `ShowCardAction` objects. These objects then host the child card in their `Card` property: + +````C# +AdaptiveCard card = new AdaptiveCard() +{ + Body = new List() { /* */ }, + // Buttons + Actions = new List() { + new ShowCardAction() + { + Title = "Hotels", + Speak = "Hotels", + Card = GetHotelSearchCard() + }, + new ShowCardAction() + { + Title = "Flights", + Speak = "Flights", + Card = new AdaptiveCard() + { + Body = new List() + { + new TextBlock() + { + Text = "Flights is not implemented =(", + Speak = "Flights is not implemented", + Weight = TextWeight.Bolder + } + } + } + } + } +}; +```` + +#### Collecting and handling input from the user + +Adaptive Cards can include input controls for gathering information from the user that is viewing the card. + +At the time of writing this sample, the Adaptive Cards support for input controls is: [Text](http://adaptivecards.io/explorer/#InputText), [Date](http://adaptivecards.io/explorer/#InputDate), [Time](http://adaptivecards.io/explorer/#InputTime), [Number](http://adaptivecards.io/explorer/#InputNumber) and for selecting options there are the [Toggle](http://adaptivecards.io/explorer/#InputToggle) and [ChoiceSet](http://adaptivecards.io/explorer/#InputChoiceSet). + +See [hotel's search form](Dialogs/RootDialog.cs#L191-L235) for a simple sample: + +````C# +new AdaptiveCard() +{ + Body = new List() + { + // Hotels Search form + new TextBlock() + { + Text = "Welcome to the Hotels finder!", + Speak = "Welcome to the Hotels finder!", + Weight = TextWeight.Bolder, + Size = TextSize.Large + }, + new TextBlock() { Text = "Please enter your destination:" }, + new TextInput() + { + Id = "Destination", + Speak = "Please enter your destination", + Placeholder = "Miami, Florida", + Style = TextInputStyle.Text + }, + new TextBlock() { Text = "When do you want to check in?" }, + new DateInput() + { + Id = "Checkin", + Speak = "When do you want to check in?" + }, + new TextBlock() { Text = "How many nights do you want to stay?" }, + new NumberInput() + { + Id = "Nights", + Min = 1, + Max = 60, + Speak = "How many nights do you want to stay?" + } + }, + Actions = new List() + { + new SubmitAction() + { + Title = "Search", + Speak = "Search", + DataJson = "{ \"Type\": \"HotelSearch\" }" + } + } +}; +```` + +The above card will generate a card similar to this one: + +![Search Form Card](images/search-form-card.png) + +Submitting the information can be be done in two possible ways: + +- **Http** + + Action.Http represents the properties needed to do an Http request. All input properties are available for use via data binding. Properties can be data bound to the Uri and Body properties, allowing you to send a request to an arbitrary url. This method can be used to call a service hosted elsewhere through HTTP. + +- **Submit** + + Action.Submit gathers up input fields, merges with optional data field and generates event to client asking for data to be submitted. The Bot Framework will send an activity through the messaging medium to the bot. This is the method used in the sample. + +When using the **Submit** method, the Bot Framework will handle the submission and your bot will receive a new `IMessageActivity` with its `Value` property filled with the form data as a dynamic object. + +````C# +public virtual async Task MessageReceivedAsync(IDialogContext context, IAwaitable result) +{ + var message = await result; + + if (message.Value != null) + { + // Got an Action Submit + dynamic value = message.Value; + string submitType = value.Type.ToString(); + switch (submitType) + { + case "HotelSearch": + /* */ + return; + + case "HotelSelection": + /* */ + return; + } + } + + // ... +} +```` + +![Search Form Submission](images/search-form-submit.png) + +You'll note in the `SumitAction` that there is a `DataJson` property with a JSON object as `{ "Type": "HotelSearch }`. The `Type` attribute is later used to identify the originating submit action. When submitting, the Adaptive Card combines the form values to the [Submit Action's `DataJson` property](http://adaptivecards.io/explorer/#ActionSubmit). + +Once received the search form parameters, [validation is triggered](Dialogs/RootDialog.cs#L43-L52), and once it passes, the [`HotelsDialog`](Dialogs/HotelsDialog.cs) is invoked with the same `IMessageActivity` object containing the search parameters: + +````C# +await context.Forward(new HotelsDialog(), this.ResumeAfterOptionDialog, message, CancellationToken.None); +```` + +#### Displaying information with ColumnSet + +For displaying the hotel search results, the sample uses `ColumnSet` and `Columns` to format them into rows and columns. See how the [`HotelsDialog`](Dialogs/HotelsDialog.cs#L49-L59) makes use of these elements to create the layout depicted below: + +````C# +// Hotels in rows of three +var rows = Split(hotels, 3) + .Select(group => new ColumnSet() + { + Columns = new List(group.Select(AsHotelItem)) + }); + +var card = new AdaptiveCard() +{ + Body = intro.Union(rows).ToList() +}; + +// ... + +private Column AsHotelItem(Hotel hotel) +{ + var submitActionData = JObject.Parse("{ \"Type\": \"HotelSelection\" }"); + submitActionData.Merge(JObject.FromObject(hotel)); + + return new Column() + { + Size = "20", + Items = new List() + { + new TextBlock() + { + Text = hotel.Name, + Speak = $"{hotel.Name}", + HorizontalAlignment = HorizontalAlignment.Center, + Wrap = false, + Weight = TextWeight.Bolder + }, + new Image() + { + Size = ImageSize.Auto, + Url = hotel.Image + } + }, + SelectAction = new SubmitAction() + { + DataJson = submitActionData.ToString() + } + }; +} +```` + +![Search Results Layout](images/search-results-layout.png) + +### Outcome + +You will see the following in the Bot Framework Emulator when opening and running the sample. + +![Sample Outcome Welcome](images/outcome-1.png) + +![Sample Outcome Results](images/outcome-2.png) + +### More Information + +To get more information about how to get started in Bot Builder for Node and Attachments please review the following resources: +* [Bot Builder for .NET](https://docs.microsoft.com/en-us/bot-framework/dotnet/) +* [Microsoft.AdaptiveCards NuGet](https://www.nuget.org/packages/Microsoft.AdaptiveCards/) +* [Adaptive Cards](http://adaptivecards.io/) +* [Adaptive Cards Visualizer](http://adaptivecards.io/visualizer/) +* [Adaptive Cards Schema Explorer](http://adaptivecards.io/explorer/) +* [Send an Adaptive Card](https://docs.microsoft.com/en-us/bot-framework/dotnet/bot-builder-dotnet-add-rich-card-attachments#a-idadaptive-carda-add-an-adaptive-card-to-a-message) + +> **Limitations** +> The functionality provided in this sample only works with WebChat and the Emulator. Other channels have limited functionality as described in the following [link](https://github.com/Microsoft/AdaptiveCards/issues/367). \ No newline at end of file diff --git a/CSharp/cards-AdaptiveCards/Web.Debug.config b/CSharp/cards-AdaptiveCards/Web.Debug.config new file mode 100644 index 0000000000..2e302f9f95 --- /dev/null +++ b/CSharp/cards-AdaptiveCards/Web.Debug.config @@ -0,0 +1,30 @@ + + + + + + + + + + \ No newline at end of file diff --git a/CSharp/cards-AdaptiveCards/Web.Release.config b/CSharp/cards-AdaptiveCards/Web.Release.config new file mode 100644 index 0000000000..c35844462b --- /dev/null +++ b/CSharp/cards-AdaptiveCards/Web.Release.config @@ -0,0 +1,31 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/CSharp/cards-AdaptiveCards/Web.config b/CSharp/cards-AdaptiveCards/Web.config new file mode 100644 index 0000000000..1a22797a7a --- /dev/null +++ b/CSharp/cards-AdaptiveCards/Web.config @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CSharp/cards-AdaptiveCards/azuredeploy.json b/CSharp/cards-AdaptiveCards/azuredeploy.json new file mode 100644 index 0000000000..cc1fb310b2 --- /dev/null +++ b/CSharp/cards-AdaptiveCards/azuredeploy.json @@ -0,0 +1,121 @@ +{ + "$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "siteName": { + "defaultValue": "BotBuilder-Samples", + "type": "string" + }, + "hostingPlanName": { + "type": "string" + }, + "siteLocation": { + "type": "string" + }, + "sku": { + "type": "string", + "allowedValues": [ + "Free", + "Shared", + "Basic", + "Standard" + ], + "defaultValue": "Free" + }, + "workerSize": { + "type": "string", + "allowedValues": [ + "0", + "1", + "2" + ], + "defaultValue": "0" + }, + "repoUrl": { + "type": "string" + }, + "branch": { + "type": "string" + }, + "Project": { + "type": "string", + "defaultValue": "CSharp/cards-AdaptiveCards" + }, + "MicrosoftAppId": { + "type": "string" + }, + "MicrosoftAppPassword": { + "type": "string" + } + }, + "resources": [ + { + "apiVersion": "2014-06-01", + "name": "[parameters('hostingPlanName')]", + "type": "Microsoft.Web/serverFarms", + "location": "[parameters('siteLocation')]", + "properties": { + "name": "[parameters('hostingPlanName')]", + "sku": "[parameters('sku')]", + "workerSize": "[parameters('workerSize')]", + "numberOfWorkers": 1 + } + }, + { + "apiVersion": "2014-06-01", + "name": "[parameters('siteName')]", + "type": "Microsoft.Web/Sites", + "location": "[parameters('siteLocation')]", + "dependsOn": [ + "[concat('Microsoft.Web/serverFarms/', parameters('hostingPlanName'))]" + ], + "tags": { + "[concat('hidden-related:', resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]": "empty" + }, + "properties": { + "name": "[parameters('siteName')]", + "serverFarm": "[parameters('hostingPlanName')]" + }, + "resources": [ + { + "apiVersion": "2014-04-01", + "type": "config", + "name": "web", + "dependsOn": [ + "[concat('Microsoft.Web/Sites/', parameters('siteName'))]" + ], + "properties": { + "appSettings": [ + { + "name": "Project", + "value": "[parameters('Project')]" + }, + { + "name": "MicrosoftAppId", + "value": "[parameters('MicrosoftAppId')]" + }, + { + "name": "MicrosoftAppPassword", + "value": "[parameters('MicrosoftAppPassword')]" + } + ] + } + }, + { + "apiVersion": "2014-04-01", + "name": "web", + "type": "sourcecontrols", + "dependsOn": [ + "[resourceId('Microsoft.Web/Sites', parameters('siteName'))]", + "[concat('Microsoft.Web/Sites/', parameters('siteName'), '/config/web')]" + ], + "properties": { + "RepoUrl": "[parameters('repoUrl')]", + "branch": "[parameters('branch')]", + "IsManualIntegration": true + } + } + ] + } + ] +} \ No newline at end of file diff --git a/CSharp/cards-AdaptiveCards/default.htm b/CSharp/cards-AdaptiveCards/default.htm new file mode 100644 index 0000000000..e84c420eda --- /dev/null +++ b/CSharp/cards-AdaptiveCards/default.htm @@ -0,0 +1,12 @@ + + + + + + + +

BotBuilder.Samples.AdaptiveCards

+

A sample bot showing AdaptiveCards.

+

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

+ + diff --git a/CSharp/cards-AdaptiveCards/images/bot_icon.png b/CSharp/cards-AdaptiveCards/images/bot_icon.png new file mode 100644 index 0000000000..ace66f9b5a Binary files /dev/null and b/CSharp/cards-AdaptiveCards/images/bot_icon.png differ diff --git a/CSharp/cards-AdaptiveCards/images/outcome-1.png b/CSharp/cards-AdaptiveCards/images/outcome-1.png new file mode 100644 index 0000000000..52992c6ccb Binary files /dev/null and b/CSharp/cards-AdaptiveCards/images/outcome-1.png differ diff --git a/CSharp/cards-AdaptiveCards/images/outcome-2.png b/CSharp/cards-AdaptiveCards/images/outcome-2.png new file mode 100644 index 0000000000..063eb5c128 Binary files /dev/null and b/CSharp/cards-AdaptiveCards/images/outcome-2.png differ diff --git a/CSharp/cards-AdaptiveCards/images/search-form-card.png b/CSharp/cards-AdaptiveCards/images/search-form-card.png new file mode 100644 index 0000000000..720b558b7f Binary files /dev/null and b/CSharp/cards-AdaptiveCards/images/search-form-card.png differ diff --git a/CSharp/cards-AdaptiveCards/images/search-form-submit.png b/CSharp/cards-AdaptiveCards/images/search-form-submit.png new file mode 100644 index 0000000000..c6dc7c352f Binary files /dev/null and b/CSharp/cards-AdaptiveCards/images/search-form-submit.png differ diff --git a/CSharp/cards-AdaptiveCards/images/search-results-layout.png b/CSharp/cards-AdaptiveCards/images/search-results-layout.png new file mode 100644 index 0000000000..284d228dd7 Binary files /dev/null and b/CSharp/cards-AdaptiveCards/images/search-results-layout.png differ diff --git a/CSharp/cards-AdaptiveCards/images/welcome-card.png b/CSharp/cards-AdaptiveCards/images/welcome-card.png new file mode 100644 index 0000000000..ed52367136 Binary files /dev/null and b/CSharp/cards-AdaptiveCards/images/welcome-card.png differ diff --git a/CSharp/cards-AdaptiveCards/packages.config b/CSharp/cards-AdaptiveCards/packages.config new file mode 100644 index 0000000000..d662f1cca3 --- /dev/null +++ b/CSharp/cards-AdaptiveCards/packages.config @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file