diff --git a/source/uwp/Visualizer/App.xaml b/source/uwp/Visualizer/App.xaml index 2b703c8597..607e834d4f 100644 --- a/source/uwp/Visualizer/App.xaml +++ b/source/uwp/Visualizer/App.xaml @@ -3,6 +3,17 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:XamlCardVisualizer" - RequestedTheme="Light"> + RequestedTheme="Light" + xmlns:converters="using:XamlCardVisualizer.Converters"> + + + + + + + diff --git a/source/uwp/Visualizer/Converters/ErrorViewModelConverters.cs b/source/uwp/Visualizer/Converters/ErrorViewModelConverters.cs new file mode 100644 index 0000000000..8ec3542f93 --- /dev/null +++ b/source/uwp/Visualizer/Converters/ErrorViewModelConverters.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Windows.UI; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml.Media; +using XamlCardVisualizer.ViewModel; + +namespace XamlCardVisualizer.Converters +{ + public class ErrorViewModelTypeToIconBackgroundConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, string language) + { + if (value is ErrorViewModelType) + { + switch ((ErrorViewModelType)value) + { + case ErrorViewModelType.Error: + case ErrorViewModelType.ErrorButRenderAllowed: + return new SolidColorBrush(Colors.Red); + + case ErrorViewModelType.Warning: + return new SolidColorBrush(Colors.Orange); + + default: + throw new NotImplementedException(); + } + } + + return value; + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + throw new NotImplementedException(); + } + } + + public sealed class ErrorViewModelTypeToIconForegroundConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, string language) + { + if (value is ErrorViewModelType) + { + switch ((ErrorViewModelType)value) + { + case ErrorViewModelType.Error: + case ErrorViewModelType.ErrorButRenderAllowed: + return new SolidColorBrush(Colors.White); + + case ErrorViewModelType.Warning: + return new SolidColorBrush(Colors.Black); + + default: + throw new NotImplementedException(); + } + } + + return value; + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + throw new NotImplementedException(); + } + } + + public sealed class ErrorViewModelTypeToSymbolConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, string language) + { + if (value is ErrorViewModelType) + { + switch ((ErrorViewModelType)value) + { + case ErrorViewModelType.Error: + case ErrorViewModelType.ErrorButRenderAllowed: + return Symbol.Cancel; + + case ErrorViewModelType.Warning: + return Symbol.Important; + + default: + throw new NotImplementedException(); + } + } + + return value; + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + throw new NotImplementedException(); + } + } +} diff --git a/source/uwp/Visualizer/Converters/NotNullToVisibilityConverter.cs b/source/uwp/Visualizer/Converters/NotNullToVisibilityConverter.cs new file mode 100644 index 0000000000..ed2516b36e --- /dev/null +++ b/source/uwp/Visualizer/Converters/NotNullToVisibilityConverter.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Data; + +namespace XamlCardVisualizer.Converters +{ + public class NotNullToVisibilityConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, string language) + { + return value != null ? Visibility.Visible : Visibility.Collapsed; + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + throw new NotImplementedException(); + } + } +} diff --git a/source/uwp/Visualizer/DocumentView.xaml b/source/uwp/Visualizer/DocumentView.xaml new file mode 100644 index 0000000000..50475a48a2 --- /dev/null +++ b/source/uwp/Visualizer/DocumentView.xaml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/uwp/Visualizer/DocumentView.xaml.cs b/source/uwp/Visualizer/DocumentView.xaml.cs new file mode 100644 index 0000000000..4fecd17ec0 --- /dev/null +++ b/source/uwp/Visualizer/DocumentView.xaml.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +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 XamlCardVisualizer.Helpers; +using XamlCardVisualizer.ViewModel; + +// The User Control item template is documented at https://go.microsoft.com/fwlink/?LinkId=234236 + +namespace XamlCardVisualizer +{ + public sealed partial class DocumentView : UserControl + { + public DocumentViewModel ViewModel + { + get { return DataContext as DocumentViewModel; } + } + + public DocumentView() + { + this.InitializeComponent(); + } + } +} diff --git a/source/uwp/Visualizer/Helpers/BindableBase.cs b/source/uwp/Visualizer/Helpers/BindableBase.cs new file mode 100644 index 0000000000..1b700c4d84 --- /dev/null +++ b/source/uwp/Visualizer/Helpers/BindableBase.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace XamlCardVisualizer.Helpers +{ + public abstract class BindableBase : INotifyPropertyChanged + { + public event PropertyChangedEventHandler PropertyChanged; + + protected void SetProperty(ref T property, T value, [CallerMemberName]string propertyName = null) + { + if (object.Equals(property, value)) + { + return; + } + + property = value; + NotifyPropertyChanged(propertyName); + } + + protected void NotifyPropertyChanged(string propertyName) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/source/uwp/Visualizer/Helpers/IListExtensions.cs b/source/uwp/Visualizer/Helpers/IListExtensions.cs new file mode 100644 index 0000000000..b9fcc6c156 --- /dev/null +++ b/source/uwp/Visualizer/Helpers/IListExtensions.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace XamlCardVisualizer.Helpers +{ + public static class IListExtensions + { + public static bool MakeListLike(this IList list, IList desired) + { + // If already equal, do nothing + if (desired.SequenceEqual(list)) + return false; + + // Remove any of the items that aren't there anymore + for (int i = 0; i < list.Count; i++) + if (!desired.Contains(list[i])) + { + list.RemoveAt(i); + i--; + } + + for (int i = 0; i < desired.Count; i++) + { + if (i >= list.Count) + list.Add(desired[i]); + + // There's a wrong item in its place + else if (!object.Equals(list[i], desired[i])) + { + // If it's already in the list somewhere, we remove it + list.Remove(desired[i]); + + // No matter what we insert it into its desired spot + list.Insert(i, desired[i]); + } + + // Otherwise it's already in the right place! + } + + return true; + } + } +} diff --git a/source/uwp/Visualizer/Helpers/PayloadValidator.cs b/source/uwp/Visualizer/Helpers/PayloadValidator.cs new file mode 100644 index 0000000000..ca03b6d57d --- /dev/null +++ b/source/uwp/Visualizer/Helpers/PayloadValidator.cs @@ -0,0 +1,244 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Schema; +using Newtonsoft.Json.Serialization; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Windows.Storage; +using XamlCardVisualizer.ViewModel; + +namespace XamlCardVisualizer.Helpers +{ + public static class PayloadValidator + { + public static async Task> ValidateAsync(string payload) + { + List errors = new List(); + + JObject parsedObject = null; + + try + { + parsedObject = JObject.Parse(payload); + } + catch (JsonReaderException ex) + { + errors.Add(new ErrorViewModel() + { + Message = "Parse error: " + ex.Message, + Type = ErrorViewModelType.Error + }); + } + catch (Exception ex) + { + errors.Add(new ErrorViewModel() + { + Message = "Parse error: " + ex.ToString(), + Type = ErrorViewModelType.Error + }); + } + + if (parsedObject != null) + { + string schema = null; + try + { + schema = await GetSchemaAsync(); + } + catch { } + + if (schema != null) + { + JSchema jsonSchema = null; + + try + { + jsonSchema = JSchema.Parse(schema); + } + catch + { + // We don't report this error to user, it's a coding error + if (Debugger.IsAttached) + { + Debugger.Break(); + } + } + + if (jsonSchema != null) + { + parsedObject.IsValid(jsonSchema, out IList validationErrors); + + foreach (var error in validationErrors) + { + + var err = error; + if (err.ChildErrors.Count > 0) + { + err = error.ChildErrors.Last(); + } + var distinct = error.ChildErrors.Distinct().ToArray(); + + var finalErrors = GetLowestMostErrors(error).Distinct(new ErrorEqualityComparer()).ToArray(); + + var typeError = finalErrors.FirstOrDefault(i => i.Path.EndsWith(".type") && i.ErrorType == ErrorType.Enum); + if (typeError != null) + { + JObject definitions = jsonSchema.ExtensionData["definitions"] as JObject; + JObject definition = definitions[typeError.Value.ToString()] as JObject; + if (definition != null) + { + bool loggedErrorForType = false; + var errorsSpecificToType = finalErrors.Where(i => object.Equals(i.Schema.Description, definition["description"]?.ToString())).ToArray(); + if (errorsSpecificToType.Length > 0) + { + foreach (var typeErrorChild in errorsSpecificToType) + { + if (typeErrorChild.ErrorType == ErrorType.Required && typeErrorChild.Message.StartsWith("Required properties are missing from object:")) + { + errors.Add(new ErrorViewModel() + { + Message = typeErrorChild.Message.Replace("Required properties are missing from object:", $"Required properties are missing from {typeError.Value}:"), + Type = ErrorViewModelType.ErrorButRenderAllowed, + Position = GetPositionInfo(typeErrorChild) + }); + loggedErrorForType = true; + } + } + } + + foreach (var parseTypeError in finalErrors.Where(i => i.ErrorType == ErrorType.Type)) + { + errors.Add(new ErrorViewModel() + { + Message = parseTypeError.Message.Replace("Invalid type", $"Invalid value type on {typeError.Value}.{GetPropertyName(parseTypeError)}"), + Type = ErrorViewModelType.ErrorButRenderAllowed, + Position = GetPositionInfo(typeError) + }); + loggedErrorForType = true; + } + + if (!loggedErrorForType) + { + errors.Add(new ErrorViewModel() + { + Message = "Unknown properties or values inside " + typeError.Value.ToString(), + Type = ErrorViewModelType.Warning, + Position = GetPositionInfo(typeError) + }); + } + continue; + } + + errors.Add(new ErrorViewModel() + { + Message = "Invalid type: " + typeError.Value.ToString(), + Type = ErrorViewModelType.Warning, + Position = GetPositionInfo(typeError) + }); + } + } + } + } + } + + return errors; + } + + private static string GetPropertyName(ValidationError error) + { + return error.Path.Split('.').LastOrDefault(); + } + + private static ErrorViewModelPositionInfo GetPositionInfo(ValidationError error) + { + return new ErrorViewModelPositionInfo() + { + LineNumber = error.LineNumber + }; + } + + private class ErrorEqualityComparer : IEqualityComparer + { + public bool Equals(ValidationError x, ValidationError y) + { + return x.Message.Equals(y.Message) + && x.LineNumber == y.LineNumber + && x.LinePosition == y.LinePosition + && x.ErrorType == y.ErrorType + && x.Path == y.Path + && x.ChildErrors.Count == y.ChildErrors.Count; + } + + public int GetHashCode(ValidationError obj) + { + return (obj.Message + + obj.LineNumber.ToString() + + obj.LinePosition.ToString() + + obj.ErrorType.ToString() + + obj.Path + + obj.ChildErrors.Count.ToString()).GetHashCode(); + } + } + + private static IEnumerable GetLowestMostErrors(ValidationError error) + { + if (error.ChildErrors.Count == 0) + { + yield return error; + yield break; + } + + foreach (var child in error.ChildErrors) + { + foreach (var lowest in GetLowestMostErrors(child)) + { + yield return lowest; + } + } + } + + private static IEnumerable GetAllErrors(ValidationError error) + { + yield return error; + foreach (var descendant in GetDescendantErrors(error)) + { + yield return descendant; + } + } + + private static IEnumerable GetDescendantErrors(ValidationError error) + { + foreach (var child in error.ChildErrors) + { + yield return child; + + foreach (var descendant in GetDescendantErrors(child)) + { + yield return descendant; + } + } + } + + private static Task _getSchemaTask; + private static Task GetSchemaAsync() + { + if (_getSchemaTask == null) + { + _getSchemaTask = GetSchemaHelperAsync(); + } + + return _getSchemaTask; + } + + private static async Task GetSchemaHelperAsync() + { + var file = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///Schemas/adaptive-card.json")); + return await FileIO.ReadTextAsync(file); + } + } +} diff --git a/source/uwp/Visualizer/MainPage.xaml b/source/uwp/Visualizer/MainPage.xaml index 63b09c3cbc..eb2f27e6e4 100644 --- a/source/uwp/Visualizer/MainPage.xaml +++ b/source/uwp/Visualizer/MainPage.xaml @@ -6,6 +6,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> +