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.
309 lines
12 KiB
309 lines
12 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(); |
|
grdTemplated.Children.Clear(); |
|
|
|
// render the adaptive card content |
|
if (txtInput.Text.Length > 0) |
|
{ |
|
// 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); |
|
} |
|
} |
|
|
|
// update the XAML layout |
|
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; |
|
} |
|
} |
|
}
|
|
|