using System; using System.Collections.Generic; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using AdaptiveCards.Rendering.Uwp; using Monaco; using Monaco.Helpers; using Newtonsoft.Json; // probs don't need these using System.IO; using System.Linq; using System.Runtime.InteropServices.WindowsRuntime; using System.Threading.Tasks; using Windows.Foundation; using Windows.Foundation.Collections; using Windows.UI.Xaml.Controls.Primitives; using Windows.UI.Xaml.Data; using Windows.UI.Xaml.Input; using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Navigation; using Monaco.Editor; using Monaco.Languages; using Newtonsoft.Json.Linq; // The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x409 namespace Adaptive_Card_Editor_UWP { /// /// An empty page that can be used on its own or navigated to within a Frame. /// public sealed partial class MainPage : Page { /// /// Create DispatcherTimer for handling input delay logic /// public DispatcherTimer keyTimer = new DispatcherTimer(); /// /// timestamp in ticks (hundred nanoseconds) of last captured KeyUp event /// public long lastKeyUp; /// /// boolean indicating whether input has changed since last tick /// public bool isRendered; /// /// timer interval in ticks (hundred nanoseconds) /// public long interval = 5000000; /// /// list of known entities that contain templated content /// /// /// If the data referenced in the container doesn't exist, the container shouldn't be included in the rendered output /// public List Containers = new List { "ActionSet", "Container", "ColumnSet", "Column", "FactSheet", "Fact", "ImageSet" }; public MainPage() { // set up a window timer for handling the keyup delay TimerSetup(); this.InitializeComponent(); } /// /// Sets up a window timer with a 500ms tick interval and starts the timer /// public void TimerSetup() { // on every timer tick, run TimerTick() keyTimer.Tick += TimerTick; // set the timer interval to half a second keyTimer.Interval = TimeSpan.Parse("00:00:00.05"); // start the timer keyTimer.Start(); } /// /// fires when txtInput sees keyboard input /// private void txtInput_KeyDown(CodeEditor sender, WebKeyEventArgs e) { // last key up event = integer value of current time lastKeyUp = DateTime.Now.Ticks; // user is inputting text, so we're going to rerender isRendered = false; } /// /// fires when txtData sees keyboard input /// private void txtData_KeyDown(CodeEditor sender, WebKeyEventArgs args) { // last key up event = integer value of current time lastKeyUp = DateTime.Now.Ticks; // user is inputting text, so we're going to rerender isRendered = false; } /// /// Fires when the timer ticks /// void TimerTick(object sender, object args) { // if isRendered is true, there have been no changes to input since the last tick if (isRendered) { return; } // otherwise, we done got input, so do the thing else { if (DateTime.Now.Ticks >= lastKeyUp + interval ) { // we done got some input! let's render it. // we don't care what the input is; if it's not valid JSON ignore this keystroke // render the text as a plain textbox TextBlock cardOutput = new TextBlock(); cardOutput.TextWrapping = TextWrapping.Wrap; cardOutput.Padding = new Thickness(10); cardOutput.Text = txtInput.Text; // clear the grid of existing content grdCard.Children.Clear(); // render an adaptive card try { AdaptiveCardRenderer cardRenderer = new AdaptiveCardRenderer(); AdaptiveCardParseResult parsedCard = AdaptiveCard.FromJsonString(txtInput.Text); RenderedAdaptiveCard theCard = cardRenderer.RenderAdaptiveCard(parsedCard.AdaptiveCard); grdCard.Children.Add(theCard.FrameworkElement); } catch (Exception ex) { // this means bad data was ingested by the adaptive card renderer // so just display the exception details cardOutput.Text = ex.ToString(); grdCard.Children.Add(cardOutput); } // render a card using template and data try { string template = txtInput.Text; string data = txtData.Text; string rendered = JsonFromTemplate(template, data); // render the card from the rendered template + data AdaptiveCardRenderer cardRenderer = new AdaptiveCardRenderer(); AdaptiveCardParseResult parsedCard = AdaptiveCard.FromJsonString(rendered); RenderedAdaptiveCard theCard = cardRenderer.RenderAdaptiveCard(parsedCard.AdaptiveCard); grdTemplated.Children.Add(theCard.FrameworkElement); } catch (Exception ex) { // render the text as a plain textbox TextBlock templateOutput = new TextBlock(); templateOutput.TextWrapping = TextWrapping.Wrap; templateOutput.Padding = new Thickness(10); templateOutput.Text = txtInput.Text; // this means bad data was ingested by the adaptive card renderer // so just display the exception details templateOutput.Text = ex.ToString(); grdTemplated.Children.Add(templateOutput); } grdCard.UpdateLayout(); isRendered = true; } else { return; } } } /// /// Creates an adaptive card JSON payload from a template and data model. Follows the official templating language. /// /// JSON adaptive card template /// JSON data model /// JSON string to be ingested by adaptive card renderer public string JsonFromTemplate(string strTemplate, string strData) { string output = ""; // first create JSON objects out of the input JObject Template = JObject.Parse(strTemplate); JObject Data = JObject.Parse(strData); // replace instances of $data with x-data // this is to work around JSON Path limitations strTemplate = strTemplate.Replace("$", "x-"); // first create JSON objects out of the input JObject template = JObject.Parse(strTemplate); JObject data = JObject.Parse(strData); // this is the old way - regex against the string // maybe do it a better way string queryString = @"..[?(@ =~ /\{(.+)?\}/)]"; List nodeList = template.SelectTokens(queryString).ToList(); // for each templated entity // retrieve entity from data JSON // if empty, remove entity foreach (JValue node in nodeList) { // get string literal for template entity string fulltext = node.Value(); // find the matching element in the data JSON // strip { and } from fulltext string cleantext = fulltext.Substring(1, fulltext.Length - 2); string searchString = "$." + cleantext; // find the token in Data JSON // can be JValue or JArray JToken searchResult = data.SelectToken(searchString); // if this element exists in the data // and isn't $data // replace element with text from Data JSON // otherwise remove it if (searchResult != null) { if (searchResult.GetType() == typeof(JValue)) { string oldValue = node.Value(); string newValue = oldValue.Replace(fulltext, ((JValue)searchResult.Value()).ToString()); node.Value = node.Value().ToString().Replace(oldValue, newValue); } else if (searchResult.GetType() == typeof(JArray)) { // it's a JArray JArray searchArray = (JArray)searchResult; foreach (string item in searchArray) { JObject newItem = new JObject((JObject)node.Parent.Parent); newItem["x-data"].Parent.Remove(); newItem["text"] = item; node.Parent.Parent.Parent.Add(newItem); } node.Parent.Parent.Remove(); } } else if (fulltext.Contains("{x-")) { // this is handled differently } else { // remove this node node.Parent.Parent.Remove(); } } // now find any empty arrays and nuke their parents // keep doing this until there are no more [] left in the JSON while (template.Descendants().OfType().Where(v => v.Type == JTokenType.Array && v.Count() == 0).ToList().Count() > 0) { List EmptyNodes = template.Descendants().OfType().Where(v => v.Type == JTokenType.Array && v.Count() == 0).ToList(); foreach (JArray Node in EmptyNodes) { Node.Parent.Parent.Remove(); } } output = JsonConvert.SerializeObject(template); return output; } } }