An editor for Microsoft Adaptive Cards that supports the new templating language and DOESN'T use JavaScript, because JavaScript isn't a real programming language.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

302 lines
11 KiB

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
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainPage : Page
{
/// <summary>
/// Create DispatcherTimer for handling input delay logic
/// </summary>
public DispatcherTimer keyTimer = new DispatcherTimer();
/// <summary>
/// timestamp in ticks (hundred nanoseconds) of last captured KeyUp event
/// </summary>
public long lastKeyUp;
/// <summary>
/// boolean indicating whether input has changed since last tick
/// </summary>
public bool isRendered;
/// <summary>
/// timer interval in ticks (hundred nanoseconds)
/// </summary>
public long interval = 5000000;
/// <summary>
/// list of known entities that contain templated content
/// </summary>
/// <remarks>
/// If the data referenced in the container doesn't exist, the container shouldn't be included in the rendered output
/// </remarks>
public List<string> Containers = new List<string>
{
"ActionSet",
"Container",
"ColumnSet",
"Column",
"FactSheet",
"Fact",
"ImageSet"
};
public MainPage()
{
// set up a window timer for handling the keyup delay
TimerSetup();
this.InitializeComponent();
}
/// <summary>
/// Sets up a window timer with a 500ms tick interval and starts the timer
/// </summary>
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();
}
/// <summary>
/// fires when txtInput sees keyboard input
/// </summary>
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;
}
/// <summary>
/// fires when txtData sees keyboard input
/// </summary>
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;
}
/// <summary>
/// Fires when the timer ticks
/// </summary>
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;
}
}
}
/// <summary>
/// Creates an adaptive card JSON payload from a template and data model. Follows the official templating language.
/// </summary>
/// <param name="Template">JSON adaptive card template</param>
/// <param name="Data">JSON data model</param>
/// <returns>JSON string to be ingested by adaptive card renderer</returns>
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<JToken> 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<string>();
// 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>();
string newValue = oldValue.Replace(fulltext, ((JValue)searchResult.Value<string>()).ToString());
node.Value = node.Value<string>().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<JArray>().Where(v => v.Type == JTokenType.Array && v.Count() == 0).ToList().Count() > 0)
{
List<JArray> EmptyNodes = template.Descendants().OfType<JArray>().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;
}
}
}