diff --git a/PettingZoo/Images/Connecting.svg b/PettingZoo/Images/Busy.svg
similarity index 100%
rename from PettingZoo/Images/Connecting.svg
rename to PettingZoo/Images/Busy.svg
diff --git a/PettingZoo/PettingZoo.csproj b/PettingZoo/PettingZoo.csproj
index cc761f0..d500398 100644
--- a/PettingZoo/PettingZoo.csproj
+++ b/PettingZoo/PettingZoo.csproj
@@ -19,7 +19,7 @@
-
+
@@ -40,6 +40,7 @@
+
@@ -54,7 +55,7 @@
-
+
diff --git a/PettingZoo/Style.xaml b/PettingZoo/Style.xaml
index f26734e..a59e8d4 100644
--- a/PettingZoo/Style.xaml
+++ b/PettingZoo/Style.xaml
@@ -1,6 +1,7 @@
+ xmlns:ui="clr-namespace:PettingZoo.UI"
+ xmlns:avalonedit="http://icsharpcode.net/sharpdevelop/avalonedit">
-
+
+
\ No newline at end of file
diff --git a/PettingZoo/UI/ErrorHighlightingTransformer.cs b/PettingZoo/UI/ErrorHighlightingTransformer.cs
new file mode 100644
index 0000000..400337a
--- /dev/null
+++ b/PettingZoo/UI/ErrorHighlightingTransformer.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Windows.Media;
+using ICSharpCode.AvalonEdit.Document;
+using ICSharpCode.AvalonEdit.Rendering;
+
+namespace PettingZoo.UI
+{
+ public class ErrorHighlightingTransformer : DocumentColorizingTransformer
+ {
+ public Brush BackgroundBrush { get; set; }
+ public TextPosition? ErrorPosition { get; set; }
+
+
+ public ErrorHighlightingTransformer()
+ {
+ BackgroundBrush = new SolidColorBrush(Color.FromRgb(255, 230, 230));
+ }
+
+
+ protected override void ColorizeLine(DocumentLine line)
+ {
+ if (ErrorPosition == null)
+ return;
+
+ if (line.LineNumber != Math.Max(ErrorPosition.Value.Row, 1))
+ return;
+
+ var lineStartOffset = line.Offset;
+
+ ChangeLinePart(lineStartOffset, lineStartOffset + line.Length,
+ element =>
+ {
+ element.BackgroundBrush = BackgroundBrush;
+ });
+ }
+ }
+}
diff --git a/PettingZoo/UI/ListBoxAutoScroll.cs b/PettingZoo/UI/ListBoxAutoScroll.cs
index b378e3b..2cbba52 100644
--- a/PettingZoo/UI/ListBoxAutoScroll.cs
+++ b/PettingZoo/UI/ListBoxAutoScroll.cs
@@ -69,6 +69,8 @@ namespace PettingZoo.UI
public void Dispose()
{
BindingOperations.ClearBinding(this, ItemsSourceProperty);
+
+ GC.SuppressFinalize(this);
}
diff --git a/PettingZoo/UI/Main/MainWindow.xaml b/PettingZoo/UI/Main/MainWindow.xaml
index a5898da..b8efb72 100644
--- a/PettingZoo/UI/Main/MainWindow.xaml
+++ b/PettingZoo/UI/Main/MainWindow.xaml
@@ -79,7 +79,7 @@
-
+
diff --git a/PettingZoo/UI/Main/MainWindow.xaml.cs b/PettingZoo/UI/Main/MainWindow.xaml.cs
index fda439b..10a1e0e 100644
--- a/PettingZoo/UI/Main/MainWindow.xaml.cs
+++ b/PettingZoo/UI/Main/MainWindow.xaml.cs
@@ -115,6 +115,8 @@ namespace PettingZoo.UI.Main
private static T? GetParent(object originalSource) where T : DependencyObject
{
var current = originalSource as DependencyObject;
+ if (current is not Visual)
+ return null;
while (current != null)
{
diff --git a/PettingZoo/UI/Tab/Publisher/PayloadEditorControl.xaml b/PettingZoo/UI/Tab/Publisher/PayloadEditorControl.xaml
index 9e45427..74e5dee 100644
--- a/PettingZoo/UI/Tab/Publisher/PayloadEditorControl.xaml
+++ b/PettingZoo/UI/Tab/Publisher/PayloadEditorControl.xaml
@@ -17,15 +17,23 @@
-
+
-
-
-
-
+
+
+
+
+
-
+
+
+
diff --git a/PettingZoo/UI/Tab/Publisher/PayloadEditorControl.xaml.cs b/PettingZoo/UI/Tab/Publisher/PayloadEditorControl.xaml.cs
index 8fd4b99..68094b9 100644
--- a/PettingZoo/UI/Tab/Publisher/PayloadEditorControl.xaml.cs
+++ b/PettingZoo/UI/Tab/Publisher/PayloadEditorControl.xaml.cs
@@ -86,6 +86,9 @@ namespace PettingZoo.UI.Tab.Publisher
}
}
+
+ private readonly ErrorHighlightingTransformer errorHighlightingTransformer = new();
+
public PayloadEditorControl()
{
// Keep the exposed properties in sync with the ViewModel
@@ -133,6 +136,42 @@ namespace PettingZoo.UI.Tab.Publisher
InitializeComponent();
+ // I'm not sure how to get a standard control border, all I could find were workaround:
+ // https://social.msdn.microsoft.com/Forums/en-US/5e007497-8d5a-401d-ac5b-9e1356fe9b64/default-borderbrush-for-textbox-listbox-etc
+ // So I'll just copy it from another TextBox. I truly hate WPF some times for making standard things so complicated.
+ EditorBorder.BorderBrush = TextBoxForBorder.BorderBrush;
+
+ Editor.Options.IndentationSize = 2;
+ Editor.TextArea.TextView.LineTransformers.Add(errorHighlightingTransformer);
+
+ // Avalon doesn't play nice with bindings it seems:
+ // https://stackoverflow.com/questions/18964176/two-way-binding-to-avalonedit-document-text-using-mvvm
+ Editor.Document.Text = Payload;
+
+ Editor.TextChanged += (_, _) =>
+ {
+ Payload = Editor.Document.Text;
+ };
+
+
+ viewModel.PropertyChanged += (_, args) =>
+ {
+ if (args.PropertyName != nameof(viewModel.ValidationInfo))
+ return;
+
+ Dispatcher.Invoke(() =>
+ {
+ if (errorHighlightingTransformer.ErrorPosition == viewModel.ValidationInfo.ErrorPosition)
+ return;
+
+ errorHighlightingTransformer.ErrorPosition = viewModel.ValidationInfo.ErrorPosition;
+
+ // TODO this can probably be optimized to only redraw the affected line
+ Editor.TextArea.TextView.Redraw();
+ });
+ };
+
+
// Setting the DataContext for the UserControl is a major PITA when binding the control's properties,
// so I've moved the ViewModel one level down to get the best of both worlds...
DataContextContainer.DataContext = viewModel;
diff --git a/PettingZoo/UI/Tab/Publisher/PayloadEditorStrings.Designer.cs b/PettingZoo/UI/Tab/Publisher/PayloadEditorStrings.Designer.cs
index aff3adc..24b2de1 100644
--- a/PettingZoo/UI/Tab/Publisher/PayloadEditorStrings.Designer.cs
+++ b/PettingZoo/UI/Tab/Publisher/PayloadEditorStrings.Designer.cs
@@ -88,20 +88,29 @@ namespace PettingZoo.UI.Tab.Publisher {
}
///
- /// Looks up a localized string similar to Invalid JSON: {0}.
+ /// Looks up a localized string similar to Invalid: {0}.
///
- internal static string JsonValidationError {
+ internal static string ValidationError {
get {
- return ResourceManager.GetString("JsonValidationError", resourceCulture);
+ return ResourceManager.GetString("ValidationError", resourceCulture);
}
}
///
- /// Looks up a localized string similar to Valid JSON.
+ /// Looks up a localized string similar to Valid.
///
- internal static string JsonValidationOk {
+ internal static string ValidationOk {
get {
- return ResourceManager.GetString("JsonValidationOk", resourceCulture);
+ return ResourceManager.GetString("ValidationOk", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Validating....
+ ///
+ internal static string ValidationValidating {
+ get {
+ return ResourceManager.GetString("ValidationValidating", resourceCulture);
}
}
}
diff --git a/PettingZoo/UI/Tab/Publisher/PayloadEditorStrings.resx b/PettingZoo/UI/Tab/Publisher/PayloadEditorStrings.resx
index 0ae56b9..5523515 100644
--- a/PettingZoo/UI/Tab/Publisher/PayloadEditorStrings.resx
+++ b/PettingZoo/UI/Tab/Publisher/PayloadEditorStrings.resx
@@ -112,10 +112,10 @@
2.0
- System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
- System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
JSON
@@ -126,10 +126,13 @@
Plain text
-
- Invalid JSON: {0}
+
+ Invalid: {0}
-
- Valid JSON
+
+ Valid
+
+
+ Validating...
\ No newline at end of file
diff --git a/PettingZoo/UI/Tab/Publisher/PayloadEditorViewModel.cs b/PettingZoo/UI/Tab/Publisher/PayloadEditorViewModel.cs
index 1eb0149..fb4ed5a 100644
--- a/PettingZoo/UI/Tab/Publisher/PayloadEditorViewModel.cs
+++ b/PettingZoo/UI/Tab/Publisher/PayloadEditorViewModel.cs
@@ -1,8 +1,9 @@
using System;
using System.ComponentModel;
using System.Reactive.Linq;
-using System.Reactive.Subjects;
using System.Windows;
+using ICSharpCode.AvalonEdit.Highlighting;
+using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace PettingZoo.UI.Tab.Publisher
@@ -15,6 +16,38 @@ namespace PettingZoo.UI.Tab.Publisher
};
+ public enum ValidationStatus
+ {
+ NotSupported,
+ Validating,
+ Ok,
+ Error
+ }
+
+
+ public readonly struct ValidationInfo
+ {
+ public ValidationStatus Status { get; }
+ public string Message { get; }
+ public TextPosition? ErrorPosition { get; }
+
+
+ public ValidationInfo(ValidationStatus status, string? message = null, TextPosition? errorPosition = null)
+ {
+ Status = status;
+ Message = message ?? status switch
+ {
+ ValidationStatus.NotSupported => "",
+ ValidationStatus.Validating => PayloadEditorStrings.ValidationValidating,
+ ValidationStatus.Ok => PayloadEditorStrings.ValidationOk,
+ ValidationStatus.Error => throw new InvalidOperationException(@"Message required for Error validation status"),
+ _ => throw new ArgumentException(@"Unsupported validation status", nameof(status))
+ };
+ ErrorPosition = errorPosition;
+ }
+ }
+
+
public class PayloadEditorViewModel : BaseViewModel
{
private const string ContentTypeJson = "application/json";
@@ -24,8 +57,7 @@ namespace PettingZoo.UI.Tab.Publisher
private PayloadEditorContentType contentTypeSelection = PayloadEditorContentType.Json;
private bool fixedJson;
- private bool jsonValid = true;
- private string jsonValidationMessage;
+ private ValidationInfo validationInfo = new(ValidationStatus.Ok);
private string payload = "";
@@ -59,7 +91,7 @@ namespace PettingZoo.UI.Tab.Publisher
get => contentTypeSelection;
set
{
- if (!SetField(ref contentTypeSelection, value, otherPropertiesChanged: new [] { nameof(JsonValidationVisibility) }))
+ if (!SetField(ref contentTypeSelection, value, otherPropertiesChanged: new [] { nameof(ValidationVisibility), nameof(SyntaxHighlighting) }))
return;
ContentType = ContentTypeSelection switch
@@ -80,23 +112,22 @@ namespace PettingZoo.UI.Tab.Publisher
set => SetField(ref fixedJson, value);
}
- public Visibility JsonValidationVisibility => ContentTypeSelection == PayloadEditorContentType.Json ? Visibility.Visible : Visibility.Collapsed;
- public Visibility JsonValidationOk => JsonValid ? Visibility.Visible : Visibility.Collapsed;
- public Visibility JsonValidationError => !JsonValid ? Visibility.Visible : Visibility.Collapsed;
-
- public string JsonValidationMessage
+ public ValidationInfo ValidationInfo
{
- get => jsonValidationMessage;
- private set => SetField(ref jsonValidationMessage, value);
+ get => validationInfo;
+ private set => SetField(ref validationInfo, value, otherPropertiesChanged: new[] { nameof(ValidationOk), nameof(ValidationError), nameof(ValidationValidating), nameof(ValidationMessage) });
}
- public bool JsonValid
- {
- get => jsonValid;
- private set => SetField(ref jsonValid, value, otherPropertiesChanged: new[] { nameof(JsonValidationOk), nameof(JsonValidationError) });
- }
+ public Visibility ValidationVisibility => ContentTypeSelection == PayloadEditorContentType.Json ? Visibility.Visible : Visibility.Collapsed;
+
+ public string ValidationMessage => ValidationInfo.Message;
+
+ public Visibility ValidationOk => ValidationInfo.Status == ValidationStatus.Ok ? Visibility.Visible : Visibility.Collapsed;
+ public Visibility ValidationError => ValidationInfo.Status == ValidationStatus.Error ? Visibility.Visible : Visibility.Collapsed;
+ public Visibility ValidationValidating => ValidationInfo.Status == ValidationStatus.Validating ? Visibility.Visible : Visibility.Collapsed;
+
public Visibility ContentTypeVisibility => FixedJson ? Visibility.Collapsed : Visibility.Visible;
@@ -108,26 +139,48 @@ namespace PettingZoo.UI.Tab.Publisher
}
+ public IHighlightingDefinition? SyntaxHighlighting => ContentTypeSelection == PayloadEditorContentType.Json
+ ? HighlightingManager.Instance.GetDefinition(@"Json")
+ : null;
+
+
public PayloadEditorViewModel()
{
- jsonValidationMessage = PayloadEditorStrings.JsonValidationOk;
-
- Observable.FromEventPattern(
+ var observable = Observable.FromEventPattern(
h => PropertyChanged += h,
h => PropertyChanged -= h)
- .Where(e => e.EventArgs.PropertyName == nameof(Payload))
+ .Where(e => e.EventArgs.PropertyName == nameof(Payload));
+
+ observable
+ .Subscribe(_ => ValidatingPayload());
+
+ observable
.Throttle(TimeSpan.FromMilliseconds(500))
.Subscribe(_ => ValidatePayload());
}
+ private void ValidatingPayload()
+ {
+ if (ValidationInfo.Status == ValidationStatus.Validating)
+ return;
+
+ if (ContentTypeSelection != PayloadEditorContentType.Json)
+ {
+ ValidationInfo = new ValidationInfo(ValidationStatus.NotSupported);
+ return;
+ }
+
+ ValidationInfo = new ValidationInfo(ValidationStatus.Validating);
+ }
+
+
private void ValidatePayload()
{
if (ContentTypeSelection != PayloadEditorContentType.Json)
{
- JsonValid = true;
- JsonValidationMessage = PayloadEditorStrings.JsonValidationOk;
+ ValidationInfo = new ValidationInfo(ValidationStatus.NotSupported);
return;
}
@@ -136,13 +189,15 @@ namespace PettingZoo.UI.Tab.Publisher
if (!string.IsNullOrEmpty(Payload))
JToken.Parse(Payload);
- JsonValid = true;
- JsonValidationMessage = PayloadEditorStrings.JsonValidationOk;
+ ValidationInfo = new ValidationInfo(ValidationStatus.Ok);
+ }
+ catch (JsonReaderException e)
+ {
+ ValidationInfo = new ValidationInfo(ValidationStatus.Error, e.Message, new TextPosition(e.LineNumber, e.LinePosition));
}
catch (Exception e)
{
- JsonValid = false;
- JsonValidationMessage = string.Format(PayloadEditorStrings.JsonValidationError, e.Message);
+ ValidationInfo = new ValidationInfo(ValidationStatus.Error, e.Message);
}
}
}
diff --git a/PettingZoo/UI/TextPosition.cs b/PettingZoo/UI/TextPosition.cs
new file mode 100644
index 0000000..ed679eb
--- /dev/null
+++ b/PettingZoo/UI/TextPosition.cs
@@ -0,0 +1,43 @@
+using System;
+
+namespace PettingZoo.UI
+{
+ public readonly struct TextPosition : IEquatable
+ {
+ public int Row { get; }
+ public int Column { get; }
+
+
+ public TextPosition(int row, int column)
+ {
+ Row = row;
+ Column = column;
+ }
+
+
+ public bool Equals(TextPosition other)
+ {
+ return Row == other.Row && Column == other.Column;
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return obj is TextPosition other && Equals(other);
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(Row, Column);
+ }
+
+ public static bool operator ==(TextPosition left, TextPosition right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(TextPosition left, TextPosition right)
+ {
+ return !(left == right);
+ }
+ }
+}