diff --git a/Avalonia.sln b/Avalonia.sln
index b2245a2f89..67fc548fed 100644
--- a/Avalonia.sln
+++ b/Avalonia.sln
@@ -163,6 +163,12 @@ Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Avalonia.RenderTests", "tes
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VirtualizationTest", "samples\VirtualizationTest\VirtualizationTest.csproj", "{FBCAF3D0-2808-4934-8E96-3F607594517B}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Interop", "Interop", "{A0CC0258-D18C-4AB3-854F-7101680FC3F9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowsInteropTest", "samples\interop\WindowsInteropTest\WindowsInteropTest.csproj", "{C7A69145-60B6-4882-97D6-A3921DD43978}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GtkInteropDemo", "samples\interop\GtkInteropDemo\GtkInteropDemo.csproj", "{BD7F352C-6DC1-4740-BAF2-2D34A038728C}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.DotNetFrameworkRuntime", "src\Avalonia.DotNetFrameworkRuntime\Avalonia.DotNetFrameworkRuntime.csproj", "{4A1ABB09-9047-4BD5-A4AD-A055E52C5EE0}"
EndProject
Global
@@ -2240,6 +2246,82 @@ Global
{FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|Mono.Build.0 = Release|Any CPU
{FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|x86.ActiveCfg = Release|Any CPU
{FBCAF3D0-2808-4934-8E96-3F607594517B}.Release|x86.Build.0 = Release|Any CPU
+ {C7A69145-60B6-4882-97D6-A3921DD43978}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU
+ {C7A69145-60B6-4882-97D6-A3921DD43978}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU
+ {C7A69145-60B6-4882-97D6-A3921DD43978}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU
+ {C7A69145-60B6-4882-97D6-A3921DD43978}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU
+ {C7A69145-60B6-4882-97D6-A3921DD43978}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {C7A69145-60B6-4882-97D6-A3921DD43978}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU
+ {C7A69145-60B6-4882-97D6-A3921DD43978}.Ad-Hoc|Mono.ActiveCfg = Release|Any CPU
+ {C7A69145-60B6-4882-97D6-A3921DD43978}.Ad-Hoc|x86.ActiveCfg = Release|Any CPU
+ {C7A69145-60B6-4882-97D6-A3921DD43978}.Ad-Hoc|x86.Build.0 = Release|Any CPU
+ {C7A69145-60B6-4882-97D6-A3921DD43978}.AppStore|Any CPU.ActiveCfg = Release|Any CPU
+ {C7A69145-60B6-4882-97D6-A3921DD43978}.AppStore|Any CPU.Build.0 = Release|Any CPU
+ {C7A69145-60B6-4882-97D6-A3921DD43978}.AppStore|iPhone.ActiveCfg = Release|Any CPU
+ {C7A69145-60B6-4882-97D6-A3921DD43978}.AppStore|iPhone.Build.0 = Release|Any CPU
+ {C7A69145-60B6-4882-97D6-A3921DD43978}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {C7A69145-60B6-4882-97D6-A3921DD43978}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU
+ {C7A69145-60B6-4882-97D6-A3921DD43978}.AppStore|Mono.ActiveCfg = Release|Any CPU
+ {C7A69145-60B6-4882-97D6-A3921DD43978}.AppStore|x86.ActiveCfg = Release|Any CPU
+ {C7A69145-60B6-4882-97D6-A3921DD43978}.AppStore|x86.Build.0 = Release|Any CPU
+ {C7A69145-60B6-4882-97D6-A3921DD43978}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C7A69145-60B6-4882-97D6-A3921DD43978}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C7A69145-60B6-4882-97D6-A3921DD43978}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+ {C7A69145-60B6-4882-97D6-A3921DD43978}.Debug|iPhone.Build.0 = Debug|Any CPU
+ {C7A69145-60B6-4882-97D6-A3921DD43978}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {C7A69145-60B6-4882-97D6-A3921DD43978}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {C7A69145-60B6-4882-97D6-A3921DD43978}.Debug|Mono.ActiveCfg = Debug|Any CPU
+ {C7A69145-60B6-4882-97D6-A3921DD43978}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {C7A69145-60B6-4882-97D6-A3921DD43978}.Debug|x86.Build.0 = Debug|Any CPU
+ {C7A69145-60B6-4882-97D6-A3921DD43978}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C7A69145-60B6-4882-97D6-A3921DD43978}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C7A69145-60B6-4882-97D6-A3921DD43978}.Release|iPhone.ActiveCfg = Release|Any CPU
+ {C7A69145-60B6-4882-97D6-A3921DD43978}.Release|iPhone.Build.0 = Release|Any CPU
+ {C7A69145-60B6-4882-97D6-A3921DD43978}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {C7A69145-60B6-4882-97D6-A3921DD43978}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+ {C7A69145-60B6-4882-97D6-A3921DD43978}.Release|Mono.ActiveCfg = Release|Any CPU
+ {C7A69145-60B6-4882-97D6-A3921DD43978}.Release|x86.ActiveCfg = Release|Any CPU
+ {C7A69145-60B6-4882-97D6-A3921DD43978}.Release|x86.Build.0 = Release|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.Ad-Hoc|Mono.ActiveCfg = Release|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.Ad-Hoc|Mono.Build.0 = Release|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.Ad-Hoc|x86.ActiveCfg = Release|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.Ad-Hoc|x86.Build.0 = Release|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.AppStore|Any CPU.ActiveCfg = Release|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.AppStore|Any CPU.Build.0 = Release|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.AppStore|iPhone.ActiveCfg = Release|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.AppStore|iPhone.Build.0 = Release|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.AppStore|Mono.ActiveCfg = Release|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.AppStore|Mono.Build.0 = Release|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.AppStore|x86.ActiveCfg = Release|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.AppStore|x86.Build.0 = Release|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.Debug|iPhone.Build.0 = Debug|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.Debug|Mono.ActiveCfg = Debug|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.Debug|Mono.Build.0 = Debug|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.Debug|x86.Build.0 = Debug|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.Release|iPhone.ActiveCfg = Release|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.Release|iPhone.Build.0 = Release|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.Release|Mono.ActiveCfg = Release|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.Release|Mono.Build.0 = Release|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.Release|x86.ActiveCfg = Release|Any CPU
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C}.Release|x86.Build.0 = Release|Any CPU
{4A1ABB09-9047-4BD5-A4AD-A055E52C5EE0}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU
{4A1ABB09-9047-4BD5-A4AD-A055E52C5EE0}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU
{4A1ABB09-9047-4BD5-A4AD-A055E52C5EE0}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU
@@ -2332,5 +2414,8 @@ Global
{F1381F98-4D24-409A-A6C5-1C5B1E08BB08} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{48840EDD-24BF-495D-911E-2EB12AE75D3B} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{FBCAF3D0-2808-4934-8E96-3F607594517B} = {9B9E3891-2366-4253-A952-D08BCEB71098}
+ {A0CC0258-D18C-4AB3-854F-7101680FC3F9} = {9B9E3891-2366-4253-A952-D08BCEB71098}
+ {C7A69145-60B6-4882-97D6-A3921DD43978} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9}
+ {BD7F352C-6DC1-4740-BAF2-2D34A038728C} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9}
EndGlobalSection
EndGlobal
diff --git a/samples/BindingTest/BindingTest.csproj b/samples/BindingTest/BindingTest.csproj
index 0cdb826e11..a1d79472d8 100644
--- a/samples/BindingTest/BindingTest.csproj
+++ b/samples/BindingTest/BindingTest.csproj
@@ -50,6 +50,7 @@
True
+
..\..\packages\System.Reactive.Core.3.0.0\lib\net45\System.Reactive.Core.dll
@@ -80,7 +81,9 @@
TestItemView.xaml
-
+
+
+
diff --git a/samples/BindingTest/MainWindow.xaml b/samples/BindingTest/MainWindow.xaml
index 149625925a..02c364346d 100644
--- a/samples/BindingTest/MainWindow.xaml
+++ b/samples/BindingTest/MainWindow.xaml
@@ -70,9 +70,19 @@
-
+
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/BindingTest/ViewModels/DataAnnotationsErrorViewModel.cs b/samples/BindingTest/ViewModels/DataAnnotationsErrorViewModel.cs
new file mode 100644
index 0000000000..634498c165
--- /dev/null
+++ b/samples/BindingTest/ViewModels/DataAnnotationsErrorViewModel.cs
@@ -0,0 +1,17 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System.ComponentModel.DataAnnotations;
+
+namespace BindingTest.ViewModels
+{
+ public class DataAnnotationsErrorViewModel
+ {
+ [Phone]
+ [MaxLength(10)]
+ public string PhoneNumber { get; set; }
+
+ [Range(0, 9)]
+ public int LessThan10 { get; set; }
+ }
+}
diff --git a/samples/BindingTest/ViewModels/ExceptionPropertyErrorViewModel.cs b/samples/BindingTest/ViewModels/ExceptionErrorViewModel.cs
similarity index 79%
rename from samples/BindingTest/ViewModels/ExceptionPropertyErrorViewModel.cs
rename to samples/BindingTest/ViewModels/ExceptionErrorViewModel.cs
index 01155f1d9f..e6071e0678 100644
--- a/samples/BindingTest/ViewModels/ExceptionPropertyErrorViewModel.cs
+++ b/samples/BindingTest/ViewModels/ExceptionErrorViewModel.cs
@@ -6,7 +6,7 @@ using System;
namespace BindingTest.ViewModels
{
- public class ExceptionPropertyErrorViewModel : ReactiveObject
+ public class ExceptionErrorViewModel : ReactiveObject
{
private int _lessThan10;
@@ -21,7 +21,7 @@ namespace BindingTest.ViewModels
}
else
{
- throw new InvalidOperationException("Value must be less than 10.");
+ throw new ArgumentOutOfRangeException("Value must be less than 10.");
}
}
}
diff --git a/samples/BindingTest/ViewModels/IndeiErrorViewModel.cs b/samples/BindingTest/ViewModels/IndeiErrorViewModel.cs
new file mode 100644
index 0000000000..b4bb528abb
--- /dev/null
+++ b/samples/BindingTest/ViewModels/IndeiErrorViewModel.cs
@@ -0,0 +1,73 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using ReactiveUI;
+using System;
+using System.ComponentModel;
+using System.Collections;
+
+namespace BindingTest.ViewModels
+{
+ public class IndeiErrorViewModel : ReactiveObject, INotifyDataErrorInfo
+ {
+ private int _maximum = 10;
+ private int _value;
+ private string _valueError;
+
+ public IndeiErrorViewModel()
+ {
+ this.WhenAnyValue(x => x.Maximum, x => x.Value)
+ .Subscribe(_ => UpdateErrors());
+ }
+
+ public bool HasErrors
+ {
+ get { throw new NotImplementedException(); }
+ }
+
+ public int Maximum
+ {
+ get { return _maximum; }
+ set { this.RaiseAndSetIfChanged(ref _maximum, value); }
+ }
+
+ public int Value
+ {
+ get { return _value; }
+ set { this.RaiseAndSetIfChanged(ref _value, value); }
+ }
+
+ public event EventHandler ErrorsChanged;
+
+ public IEnumerable GetErrors(string propertyName)
+ {
+ switch (propertyName)
+ {
+ case nameof(Value):
+ return new[] { _valueError };
+ default:
+ return null;
+ }
+ }
+
+ private void UpdateErrors()
+ {
+ if (Value <= Maximum)
+ {
+ if (_valueError != null)
+ {
+ _valueError = null;
+ ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(Value)));
+ }
+ }
+ else
+ {
+ if (_valueError == null)
+ {
+ _valueError = "Value must be less than Maximum";
+ ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(Value)));
+ }
+ }
+ }
+ }
+}
diff --git a/samples/BindingTest/ViewModels/MainWindowViewModel.cs b/samples/BindingTest/ViewModels/MainWindowViewModel.cs
index 6fbfb8a23f..94f7ff595a 100644
--- a/samples/BindingTest/ViewModels/MainWindowViewModel.cs
+++ b/samples/BindingTest/ViewModels/MainWindowViewModel.cs
@@ -69,7 +69,8 @@ namespace BindingTest.ViewModels
public ReactiveCommand
+
+
+ Designer
+
+
+
+
@@ -28,8 +31,8 @@
-
+
diff --git a/src/Avalonia.Themes.Default/DropDown.xaml b/src/Avalonia.Themes.Default/DropDown.xaml
index c33e4af4f4..5a3d44360c 100644
--- a/src/Avalonia.Themes.Default/DropDown.xaml
+++ b/src/Avalonia.Themes.Default/DropDown.xaml
@@ -10,10 +10,10 @@
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Avalonia.Themes.Default/TextBox.xaml b/src/Avalonia.Themes.Default/TextBox.xaml
index c68fab958e..8a5a41845b 100644
--- a/src/Avalonia.Themes.Default/TextBox.xaml
+++ b/src/Avalonia.Themes.Default/TextBox.xaml
@@ -27,10 +27,19 @@
-
-
+
+
+
+
+
+
-
+
+
@@ -57,7 +67,17 @@
-
+
+
+
\ No newline at end of file
diff --git a/src/Gtk/Avalonia.Cairo/CairoPlatform.cs b/src/Gtk/Avalonia.Cairo/CairoPlatform.cs
index 3b0d7d08da..6606ed4aef 100644
--- a/src/Gtk/Avalonia.Cairo/CairoPlatform.cs
+++ b/src/Gtk/Avalonia.Cairo/CairoPlatform.cs
@@ -53,13 +53,21 @@ namespace Avalonia.Cairo
public IRenderTarget CreateRenderer(IPlatformHandle handle)
{
var window = handle as Gtk.Window;
- if (window == null)
- throw new NotSupportedException(string.Format(
- "Don't know how to create a Cairo renderer from a '{0}' handle which isn't Gtk.Window",
- handle.HandleDescriptor));
-
- window.DoubleBuffered = true;
- return new RenderTarget(window);
+ if (window != null)
+ {
+ window.DoubleBuffered = true;
+ return new RenderTarget(window);
+ }
+ var area = handle as Gtk.DrawingArea;
+ if (area != null)
+ {
+ area.DoubleBuffered = true;
+ return new RenderTarget(area);
+ }
+
+ throw new NotSupportedException(string.Format(
+ "Don't know how to create a Cairo renderer from a '{0}' handle which isn't Gtk.Window or Gtk.DrawingArea",
+ handle.HandleDescriptor));
}
public IRenderTargetBitmapImpl CreateRenderTargetBitmap(int width, int height)
diff --git a/src/Gtk/Avalonia.Cairo/RenderTarget.cs b/src/Gtk/Avalonia.Cairo/RenderTarget.cs
index cb7241f32e..d285986762 100644
--- a/src/Gtk/Avalonia.Cairo/RenderTarget.cs
+++ b/src/Gtk/Avalonia.Cairo/RenderTarget.cs
@@ -7,6 +7,7 @@ using Avalonia.Cairo.Media;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering;
+using Gtk;
using DrawingContext = Avalonia.Media.DrawingContext;
namespace Avalonia.Cairo
@@ -20,6 +21,7 @@ namespace Avalonia.Cairo
{
private readonly Surface _surface;
private readonly Gtk.Window _window;
+ private readonly Gtk.DrawingArea _area;
///
/// Initializes a new instance of the class.
@@ -37,19 +39,28 @@ namespace Avalonia.Cairo
_surface = surface;
}
+ public RenderTarget(DrawingArea area)
+ {
+ _area = area;
+ }
///
/// Creates a cairo surface that targets a platform-specific resource.
///
/// A surface wrapped in an .
- public DrawingContext CreateDrawingContext()
+ public DrawingContext CreateDrawingContext() => new DrawingContext(CreateMediaDrawingContext());
+
+ public IDrawingContextImpl CreateMediaDrawingContext()
{
- var ctx = _surface != null
- ? new Media.DrawingContext(_surface)
- : new Media.DrawingContext(_window.GdkWindow);
- return new DrawingContext(ctx);
+ if (_window != null)
+ return new Media.DrawingContext(_window.GdkWindow);
+ if (_surface != null)
+ return new Media.DrawingContext(_surface);
+ if (_area != null)
+ return new Media.DrawingContext(_area.GdkWindow);
+ throw new InvalidOperationException("Unspecified render target");
}
-
+
public void Dispose() => _surface?.Dispose();
}
}
diff --git a/src/Gtk/Avalonia.Gtk/Avalonia.Gtk.csproj b/src/Gtk/Avalonia.Gtk/Avalonia.Gtk.csproj
index 2a47aa99fa..7e8c118196 100644
--- a/src/Gtk/Avalonia.Gtk/Avalonia.Gtk.csproj
+++ b/src/Gtk/Avalonia.Gtk/Avalonia.Gtk.csproj
@@ -46,6 +46,8 @@
+
+
@@ -54,8 +56,10 @@
+
+
@@ -67,6 +71,10 @@
{B09B78D8-9B26-48B0-9149-D64A2F120F3F}
Avalonia.Base
+
+ {7062ae20-5dcc-4442-9645-8195bdece63e}
+ Avalonia.Diagnostics
+
{62024B2D-53EB-4638-B26B-85EEAA54866E}
Avalonia.Input
diff --git a/src/Gtk/Avalonia.Gtk/EmbeddableImpl.cs b/src/Gtk/Avalonia.Gtk/EmbeddableImpl.cs
new file mode 100644
index 0000000000..62b2513856
--- /dev/null
+++ b/src/Gtk/Avalonia.Gtk/EmbeddableImpl.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reactive.Disposables;
+using System.Text;
+using System.Threading.Tasks;
+using Avalonia.Platform;
+using Gdk;
+using Gtk;
+using Action = System.Action;
+using WindowEdge = Avalonia.Controls.WindowEdge;
+
+namespace Avalonia.Gtk
+{
+ class EmbeddableImpl : WindowImplBase, IEmbeddableWindowImpl
+ {
+ public event Action LostFocus;
+
+ public EmbeddableImpl(DrawingArea area) : base(area)
+ {
+ area.Events = EventMask.AllEventsMask;
+ area.SizeAllocated += Plug_SizeAllocated;
+ }
+
+ public EmbeddableImpl() : this(new PlatformHandleAwareDrawingArea())
+ {
+ }
+
+ private void Plug_SizeAllocated(object o, SizeAllocatedArgs args)
+ {
+ Resized?.Invoke(new Size(args.Allocation.Width, args.Allocation.Height));
+ }
+
+ public override Size ClientSize
+ {
+ get { return new Size(Widget.Allocation.Width, Widget.Allocation.Height); }
+ set {}
+ }
+
+
+ //Stubs are needed for future GTK designer embedding support
+ public override void SetTitle(string title)
+ {
+ }
+
+ public override IDisposable ShowDialog() => Disposable.Create(() => { });
+
+ public override void SetSystemDecorations(bool enabled)
+ {
+ }
+
+ public override void SetIcon(IWindowIconImpl icon)
+ {
+ }
+
+ public override void BeginMoveDrag()
+ {
+ }
+
+ public override void BeginResizeDrag(WindowEdge edge)
+ {
+ }
+
+ public override Point Position
+ {
+ get { return new Point(); }
+ set {}
+ }
+ }
+}
diff --git a/src/Gtk/Avalonia.Gtk/Embedding/GtkAvaloniaControlHost.cs b/src/Gtk/Avalonia.Gtk/Embedding/GtkAvaloniaControlHost.cs
new file mode 100644
index 0000000000..5594a3c13f
--- /dev/null
+++ b/src/Gtk/Avalonia.Gtk/Embedding/GtkAvaloniaControlHost.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Avalonia.Controls;
+using Avalonia.Controls.Embedding;
+using Avalonia.Diagnostics;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Layout;
+using Avalonia.Platform;
+using Avalonia.VisualTree;
+using Gdk;
+using Gtk;
+
+namespace Avalonia.Gtk.Embedding
+{
+ public class GtkAvaloniaControlHost : DrawingArea, IPlatformHandle
+ {
+ private EmbeddableControlRoot _root;
+
+ public GtkAvaloniaControlHost()
+ {
+ _root = new EmbeddableControlRoot(new EmbeddableImpl(this));
+ _root.Prepare();
+ if (_root.IsFocused)
+ Unfocus();
+ _root.GotFocus += RootGotFocus;
+ CanFocus = true;
+ }
+
+ void Unfocus()
+ {
+ var focused = (IVisual)FocusManager.Instance.Current;
+ if (focused == null)
+ return;
+ while (focused.VisualParent != null)
+ focused = focused.VisualParent;
+
+ if (focused == _root)
+ KeyboardDevice.Instance.SetFocusedElement(null, NavigationMethod.Unspecified, InputModifiers.None);
+ }
+
+ protected override bool OnFocusOutEvent(EventFocus evnt)
+ {
+ Unfocus();
+ return false;
+ }
+
+ private void RootGotFocus(object sender, RoutedEventArgs e)
+ {
+ this.HasFocus = true;
+ GdkWindow.Focus(0);
+ }
+
+ private Control _content;
+
+ public Control Content
+ {
+ get { return _content; }
+ set
+ {
+ _content = value;
+ if (_root != null)
+ {
+ _root.Content = value;
+ _root.Prepare();
+ }
+ }
+ }
+
+ IntPtr IPlatformHandle.Handle => PlatformHandleAwareWindow.GetNativeWindow(GdkWindow);
+
+ string IPlatformHandle.HandleDescriptor => "HWND";
+ }
+}
diff --git a/src/Gtk/Avalonia.Gtk/GtkPlatform.cs b/src/Gtk/Avalonia.Gtk/GtkPlatform.cs
index c8246fc9f5..455aa67405 100644
--- a/src/Gtk/Avalonia.Gtk/GtkPlatform.cs
+++ b/src/Gtk/Avalonia.Gtk/GtkPlatform.cs
@@ -103,10 +103,7 @@ namespace Avalonia.Gtk
return new WindowImpl();
}
- public IWindowImpl CreateEmbeddableWindow()
- {
- throw new NotSupportedException();
- }
+ public IEmbeddableWindowImpl CreateEmbeddableWindow() => new EmbeddableImpl();
public IPopupImpl CreatePopup()
{
diff --git a/src/Gtk/Avalonia.Gtk/SystemDialogImpl.cs b/src/Gtk/Avalonia.Gtk/SystemDialogImpl.cs
index a47a0a0ef8..ed92a25873 100644
--- a/src/Gtk/Avalonia.Gtk/SystemDialogImpl.cs
+++ b/src/Gtk/Avalonia.Gtk/SystemDialogImpl.cs
@@ -15,7 +15,7 @@ namespace Avalonia.Gtk
public Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent)
{
var tcs = new TaskCompletionSource();
- var dlg = new global::Gtk.FileChooserDialog(dialog.Title, ((WindowImpl)parent),
+ var dlg = new global::Gtk.FileChooserDialog(dialog.Title, ((WindowImplBase)parent).Widget.Toplevel as Window,
dialog is OpenFileDialog
? FileChooserAction.Open
: FileChooserAction.Save,
@@ -57,7 +57,7 @@ namespace Avalonia.Gtk
public Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent)
{
var tcs = new TaskCompletionSource();
- var dlg = new global::Gtk.FileChooserDialog(dialog.Title, ((WindowImpl)parent),
+ var dlg = new global::Gtk.FileChooserDialog(dialog.Title, ((WindowImplBase)parent).Widget.Toplevel as Window,
FileChooserAction.SelectFolder,
"Cancel", ResponseType.Cancel,
"Select Folder", ResponseType.Accept)
diff --git a/src/Gtk/Avalonia.Gtk/WindowImpl.cs b/src/Gtk/Avalonia.Gtk/WindowImpl.cs
index 26ad52135d..c1a69e8932 100644
--- a/src/Gtk/Avalonia.Gtk/WindowImpl.cs
+++ b/src/Gtk/Avalonia.Gtk/WindowImpl.cs
@@ -1,404 +1,118 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
using System;
using System.Reactive.Disposables;
-using System.Runtime.InteropServices;
-using Gdk;
-using Avalonia.Controls;
-using Avalonia.Input.Raw;
using Avalonia.Platform;
-using Avalonia.Input;
-using Avalonia.Threading;
-using Action = System.Action;
-using WindowEdge = Avalonia.Controls.WindowEdge;
+using Gdk;
namespace Avalonia.Gtk
{
using Gtk = global::Gtk;
-
- public class WindowImpl : Gtk.Window, IWindowImpl, IPlatformHandle
+ public class WindowImpl : WindowImplBase
{
- private IInputRoot _inputRoot;
+ private Gtk.Window _window;
+ private Gtk.Window Window => _window ?? (_window = (Gtk.Window) Widget);
- private Size _lastClientSize;
- private Gtk.IMContext _imContext;
- private uint _lastKeyEventTimestamp;
-
- private static readonly Gdk.Cursor DefaultCursor = new Gdk.Cursor(CursorType.LeftPtr);
-
- public WindowImpl()
- : base(Gtk.WindowType.Toplevel)
+ public WindowImpl(Gtk.WindowType type) : base(new PlatformHandleAwareWindow(type))
{
- DefaultSize = new Gdk.Size(900, 480);
Init();
}
- public WindowImpl(Gtk.WindowType type)
- : base(type)
+ public WindowImpl()
+ : base(new PlatformHandleAwareWindow(Gtk.WindowType.Toplevel) {DefaultSize = new Gdk.Size(900, 480)})
{
Init();
}
- private void Init()
+ void Init()
{
- Events = EventMask.PointerMotionMask |
- EventMask.ButtonPressMask |
- EventMask.ButtonReleaseMask;
- _imContext = new Gtk.IMMulticontext();
- _imContext.Commit += ImContext_Commit;
- DoubleBuffered = false;
- Realize();
+ Window.FocusActivated += OnFocusActivated;
+ Window.ConfigureEvent += OnConfigureEvent;
_lastClientSize = ClientSize;
}
-
- protected override void OnRealized ()
- {
- base.OnRealized ();
- _imContext.ClientWindow = this.GdkWindow;
- }
-
- public Size ClientSize
+ private Size _lastClientSize;
+ void OnConfigureEvent(object o, Gtk.ConfigureEventArgs args)
{
- get
- {
- int width;
- int height;
- GetSize(out width, out height);
- return new Size(width, height);
- }
-
- set
- {
- Resize((int)value.Width, (int)value.Height);
- }
- }
+ var evnt = args.Event;
+ args.RetVal = true;
+ var newSize = new Size(evnt.Width, evnt.Height);
- public Size MaxClientSize
- {
- get
+ if (newSize != _lastClientSize)
{
- // TODO: This should take into account things such as taskbar and window border
- // thickness etc.
- return new Size(Screen.Width, Screen.Height);
+ Resized(newSize);
+ _lastClientSize = newSize;
}
}
- public Avalonia.Controls.WindowState WindowState
+ public override Size ClientSize
{
get
{
- switch (GdkWindow.State)
- {
- case Gdk.WindowState.Iconified:
- return Controls.WindowState.Minimized;
- case Gdk.WindowState.Maximized:
- return Controls.WindowState.Maximized;
- default:
- return Controls.WindowState.Normal;
- }
+ int width;
+ int height;
+ Window.GetSize(out width, out height);
+ return new Size(width, height);
}
set
{
- switch (value)
- {
- case Controls.WindowState.Minimized:
- GdkWindow.Iconify();
- break;
- case Controls.WindowState.Maximized:
- GdkWindow.Maximize();
- break;
- case Controls.WindowState.Normal:
- GdkWindow.Deiconify();
- GdkWindow.Unmaximize();
- break;
- }
+ Window.Resize((int)value.Width, (int)value.Height);
}
}
- public double Scaling => 1;
-
- IPlatformHandle ITopLevelImpl.Handle => this;
-
- [DllImport("libgdk-win32-2.0-0.dll", CallingConvention = CallingConvention.Cdecl)]
- extern static IntPtr gdk_win32_drawable_get_handle(IntPtr gdkWindow);
-
- [DllImport("libgtk-x11-2.0.so.0", CallingConvention = CallingConvention.Cdecl)]
- extern static IntPtr gdk_x11_drawable_get_xid(IntPtr gdkWindow);
-
- [DllImport("libgdk-quartz-2.0-0.dylib", CallingConvention = CallingConvention.Cdecl)]
- extern static IntPtr gdk_quartz_window_get_nswindow(IntPtr gdkWindow);
-
- IntPtr _nativeWindow;
-
- IntPtr GetNativeWindow()
+ public override void SetTitle(string title)
{
- IntPtr h = GdkWindow.Handle;
- if (_nativeWindow != IntPtr.Zero)
- return _nativeWindow;
- //Try whatever backend that works
- try
- {
- return _nativeWindow = gdk_quartz_window_get_nswindow(h);
- }
- catch
- {
- }
- try
- {
- return _nativeWindow = gdk_x11_drawable_get_xid(h);
- }
- catch
- {
- }
- return _nativeWindow = gdk_win32_drawable_get_handle(h);
+ Window.Title = title;
}
-
- IntPtr IPlatformHandle.Handle => GetNativeWindow();
- public string HandleDescriptor => "HWND";
-
- public Action Activated { get; set; }
-
- public Action Closed { get; set; }
-
- public Action Deactivated { get; set; }
-
- public Action Input { get; set; }
-
- public Action Paint { get; set; }
-
- public Action Resized { get; set; }
-
- public Action ScalingChanged { get; set; }
-
- public IPopupImpl CreatePopup()
- {
- return new PopupImpl();
- }
-
- public void Invalidate(Rect rect)
- {
- if (base.GdkWindow != null)
- base.GdkWindow.InvalidateRect(
- new Rectangle((int) rect.X, (int) rect.Y, (int) rect.Width, (int) rect.Height), true);
- }
-
- public Point PointToClient(Point point)
+ void OnFocusActivated(object sender, EventArgs eventArgs)
{
- int x, y;
- GdkWindow.GetDeskrelativeOrigin(out x, out y);
-
- return new Point(point.X - x, point.Y - y);
- }
-
- public Point PointToScreen(Point point)
- {
- int x, y;
- GdkWindow.GetDeskrelativeOrigin(out x, out y);
-
- return new Point(point.X + x, point.Y + y);
- }
-
- public void SetInputRoot(IInputRoot inputRoot)
- {
- _inputRoot = inputRoot;
- }
-
- public void SetTitle(string title)
- {
- Title = title;
- }
-
-
- public void SetCursor(IPlatformHandle cursor)
- {
- GdkWindow.Cursor = cursor != null ? new Gdk.Cursor(cursor.Handle) : DefaultCursor;
+ Activated();
}
- public void BeginMoveDrag()
+ public override void BeginMoveDrag()
{
int x, y;
ModifierType mod;
- Screen.RootWindow.GetPointer(out x, out y, out mod);
- BeginMoveDrag(1, x, y, 0);
+ Window.Screen.RootWindow.GetPointer(out x, out y, out mod);
+ Window.BeginMoveDrag(1, x, y, 0);
}
- public void BeginResizeDrag(WindowEdge edge)
+ public override void BeginResizeDrag(Controls.WindowEdge edge)
{
int x, y;
ModifierType mod;
- Screen.RootWindow.GetPointer(out x, out y, out mod);
- BeginResizeDrag((Gdk.WindowEdge) (int) edge, 1, x, y, 0);
+ Window.Screen.RootWindow.GetPointer(out x, out y, out mod);
+ Window.BeginResizeDrag((Gdk.WindowEdge)(int)edge, 1, x, y, 0);
}
- public Point Position
+ public override Point Position
{
get
{
int x, y;
- GetPosition(out x, out y);
+ Window.GetPosition(out x, out y);
return new Point(x, y);
}
set
{
- Move((int)value.X, (int)value.Y);
+ Window.Move((int)value.X, (int)value.Y);
}
}
- public IDisposable ShowDialog()
+ public override IDisposable ShowDialog()
{
- Modal = true;
- Show();
+ Window.Modal = true;
+ Window.Show();
return Disposable.Empty;
}
- public void SetSystemDecorations(bool enabled) => Decorated = enabled;
-
- void ITopLevelImpl.Activate()
- {
- Activate();
- }
-
- private static InputModifiers GetModifierKeys(ModifierType state)
- {
- var rv = InputModifiers.None;
- if (state.HasFlag(ModifierType.ControlMask))
- rv |= InputModifiers.Control;
- if (state.HasFlag(ModifierType.ShiftMask))
- rv |= InputModifiers.Shift;
- if (state.HasFlag(ModifierType.Mod1Mask))
- rv |= InputModifiers.Control;
- if(state.HasFlag(ModifierType.Button1Mask))
- rv |= InputModifiers.LeftMouseButton;
- if (state.HasFlag(ModifierType.Button2Mask))
- rv |= InputModifiers.RightMouseButton;
- if (state.HasFlag(ModifierType.Button3Mask))
- rv |= InputModifiers.MiddleMouseButton;
- return rv;
- }
-
- protected override bool OnButtonPressEvent(EventButton evnt)
- {
-
- var e = new RawMouseEventArgs(
- GtkMouseDevice.Instance,
- evnt.Time,
- _inputRoot,
- evnt.Button == 1
- ? RawMouseEventType.LeftButtonDown
- : evnt.Button == 3 ? RawMouseEventType.RightButtonDown : RawMouseEventType.MiddleButtonDown,
- new Point(evnt.X, evnt.Y), GetModifierKeys(evnt.State));
- Input(e);
- return true;
- }
-
- protected override bool OnScrollEvent(EventScroll evnt)
- {
- double step = 1;
- var delta = new Vector();
- if (evnt.Direction == ScrollDirection.Down)
- delta = new Vector(0, -step);
- else if (evnt.Direction == ScrollDirection.Up)
- delta = new Vector(0, step);
- else if (evnt.Direction == ScrollDirection.Right)
- delta = new Vector(-step, 0);
- if (evnt.Direction == ScrollDirection.Left)
- delta = new Vector(step, 0);
- var e = new RawMouseWheelEventArgs(GtkMouseDevice.Instance, evnt.Time, _inputRoot, new Point(evnt.X, evnt.Y), delta, GetModifierKeys(evnt.State));
- Input(e);
- return base.OnScrollEvent(evnt);
- }
-
- protected override bool OnButtonReleaseEvent(EventButton evnt)
- {
- var e = new RawMouseEventArgs(
- GtkMouseDevice.Instance,
- evnt.Time,
- _inputRoot,
- evnt.Button == 1
- ? RawMouseEventType.LeftButtonUp
- : evnt.Button == 3 ? RawMouseEventType.RightButtonUp : RawMouseEventType.MiddleButtonUp,
- new Point(evnt.X, evnt.Y), GetModifierKeys(evnt.State));
- Input(e);
- return true;
- }
-
- protected override bool OnConfigureEvent(EventConfigure evnt)
- {
- var newSize = new Size(evnt.Width, evnt.Height);
-
- if (newSize != _lastClientSize)
- {
- Resized(newSize);
- _lastClientSize = newSize;
- }
-
- return true;
- }
-
- protected override void OnDestroyed()
- {
- Closed();
- }
-
- private bool ProcessKeyEvent(EventKey evnt)
- {
- _lastKeyEventTimestamp = evnt.Time;
- if (_imContext.FilterKeypress(evnt))
- return true;
- var e = new RawKeyEventArgs(
- GtkKeyboardDevice.Instance,
- evnt.Time,
- evnt.Type == EventType.KeyPress ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp,
- GtkKeyboardDevice.ConvertKey(evnt.Key), GetModifierKeys(evnt.State));
- Input(e);
- return true;
- }
-
- protected override bool OnKeyPressEvent(EventKey evnt) => ProcessKeyEvent(evnt);
-
- protected override bool OnKeyReleaseEvent(EventKey evnt) => ProcessKeyEvent(evnt);
-
- private void ImContext_Commit(object o, Gtk.CommitArgs args)
- {
- Input(new RawTextInputEventArgs(GtkKeyboardDevice.Instance, _lastKeyEventTimestamp, args.Str));
- }
-
- protected override bool OnExposeEvent(EventExpose evnt)
- {
- Paint(evnt.Area.ToAvalonia());
- return true;
- }
-
- protected override void OnFocusActivated()
- {
- Activated();
- }
-
- protected override bool OnMotionNotifyEvent(EventMotion evnt)
- {
- var position = new Point(evnt.X, evnt.Y);
-
- GtkMouseDevice.Instance.SetClientPosition(position);
-
- var e = new RawMouseEventArgs(
- GtkMouseDevice.Instance,
- evnt.Time,
- _inputRoot,
- RawMouseEventType.Move,
- position, GetModifierKeys(evnt.State));
- Input(e);
- return true;
- }
+ public override void SetSystemDecorations(bool enabled) => Window.Decorated = enabled;
- public void SetIcon(IWindowIconImpl icon)
+ public override void SetIcon(IWindowIconImpl icon)
{
- Icon = ((IconImpl)icon).Pixbuf;
+ Window.Icon = ((IconImpl)icon).Pixbuf;
}
}
-}
+}
\ No newline at end of file
diff --git a/src/Gtk/Avalonia.Gtk/WindowImplBase.cs b/src/Gtk/Avalonia.Gtk/WindowImplBase.cs
new file mode 100644
index 0000000000..c0020f5e6f
--- /dev/null
+++ b/src/Gtk/Avalonia.Gtk/WindowImplBase.cs
@@ -0,0 +1,310 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Reactive.Disposables;
+using System.Runtime.InteropServices;
+using Gdk;
+using Avalonia.Controls;
+using Avalonia.Input.Raw;
+using Avalonia.Platform;
+using Avalonia.Input;
+using Avalonia.Threading;
+using Action = System.Action;
+using WindowEdge = Avalonia.Controls.WindowEdge;
+
+namespace Avalonia.Gtk
+{
+ using Gtk = global::Gtk;
+
+ public abstract class WindowImplBase : IWindowImpl
+ {
+ private IInputRoot _inputRoot;
+ protected Gtk.Widget _window;
+ public Gtk.Widget Widget => _window;
+
+
+ private Gtk.IMContext _imContext;
+
+ private uint _lastKeyEventTimestamp;
+
+ private static readonly Gdk.Cursor DefaultCursor = new Gdk.Cursor(CursorType.LeftPtr);
+
+ protected WindowImplBase(Gtk.Widget window)
+ {
+ _window = window;
+ Init();
+ }
+
+ void Init()
+ {
+ Handle = _window as IPlatformHandle;
+ _window.Events = EventMask.AllEventsMask;
+ _imContext = new Gtk.IMMulticontext();
+ _imContext.Commit += ImContext_Commit;
+ _window.Realized += OnRealized;
+ _window.DoubleBuffered = false;
+ _window.Realize();
+ _window.ButtonPressEvent += OnButtonPressEvent;
+ _window.ButtonReleaseEvent += OnButtonReleaseEvent;
+ _window.ScrollEvent += OnScrollEvent;
+ _window.Destroyed += OnDestroyed;
+ _window.KeyPressEvent += OnKeyPressEvent;
+ _window.KeyReleaseEvent += OnKeyReleaseEvent;
+ _window.ExposeEvent += OnExposeEvent;
+ _window.MotionNotifyEvent += OnMotionNotifyEvent;
+
+ }
+
+ public IPlatformHandle Handle { get; private set; }
+
+ void OnRealized (object sender, EventArgs eventArgs)
+ {
+ _imContext.ClientWindow = _window.GdkWindow;
+ }
+
+ public abstract Size ClientSize { get; set; }
+
+
+ public Size MaxClientSize
+ {
+ get
+ {
+ // TODO: This should take into account things such as taskbar and window border
+ // thickness etc.
+ return new Size(_window.Screen.Width, _window.Screen.Height);
+ }
+ }
+
+ public Avalonia.Controls.WindowState WindowState
+ {
+ get
+ {
+ switch (_window.GdkWindow.State)
+ {
+ case Gdk.WindowState.Iconified:
+ return Controls.WindowState.Minimized;
+ case Gdk.WindowState.Maximized:
+ return Controls.WindowState.Maximized;
+ default:
+ return Controls.WindowState.Normal;
+ }
+ }
+
+ set
+ {
+ switch (value)
+ {
+ case Controls.WindowState.Minimized:
+ _window.GdkWindow.Iconify();
+ break;
+ case Controls.WindowState.Maximized:
+ _window.GdkWindow.Maximize();
+ break;
+ case Controls.WindowState.Normal:
+ _window.GdkWindow.Deiconify();
+ _window.GdkWindow.Unmaximize();
+ break;
+ }
+ }
+ }
+
+ public double Scaling => 1;
+
+ public Action Activated { get; set; }
+
+ public Action Closed { get; set; }
+
+ public Action Deactivated { get; set; }
+
+ public Action Input { get; set; }
+
+ public Action Paint { get; set; }
+
+ public Action Resized { get; set; }
+
+ public Action ScalingChanged { get; set; }
+
+ public IPopupImpl CreatePopup()
+ {
+ return new PopupImpl();
+ }
+
+ public void Invalidate(Rect rect)
+ {
+ if (_window.GdkWindow != null)
+ _window.GdkWindow.InvalidateRect(
+ new Rectangle((int) rect.X, (int) rect.Y, (int) rect.Width, (int) rect.Height), true);
+ }
+
+ public Point PointToClient(Point point)
+ {
+ int x, y;
+ _window.GdkWindow.GetDeskrelativeOrigin(out x, out y);
+
+ return new Point(point.X - x, point.Y - y);
+ }
+
+ public Point PointToScreen(Point point)
+ {
+ int x, y;
+ _window.GdkWindow.GetDeskrelativeOrigin(out x, out y);
+ return new Point(point.X + x, point.Y + y);
+ }
+
+ public void SetInputRoot(IInputRoot inputRoot)
+ {
+ _inputRoot = inputRoot;
+ }
+
+ public abstract void SetTitle(string title);
+ public abstract IDisposable ShowDialog();
+ public abstract void SetSystemDecorations(bool enabled);
+ public abstract void SetIcon(IWindowIconImpl icon);
+
+
+ public void SetCursor(IPlatformHandle cursor)
+ {
+ _window.GdkWindow.Cursor = cursor != null ? new Gdk.Cursor(cursor.Handle) : DefaultCursor;
+ }
+
+ public void Show() => _window.Show();
+
+ public void Hide() => _window.Hide();
+ public abstract void BeginMoveDrag();
+ public abstract void BeginResizeDrag(WindowEdge edge);
+ public abstract Point Position { get; set; }
+
+ void ITopLevelImpl.Activate()
+ {
+ _window.Activate();
+ }
+
+ private static InputModifiers GetModifierKeys(ModifierType state)
+ {
+ var rv = InputModifiers.None;
+ if (state.HasFlag(ModifierType.ControlMask))
+ rv |= InputModifiers.Control;
+ if (state.HasFlag(ModifierType.ShiftMask))
+ rv |= InputModifiers.Shift;
+ if (state.HasFlag(ModifierType.Mod1Mask))
+ rv |= InputModifiers.Control;
+ if(state.HasFlag(ModifierType.Button1Mask))
+ rv |= InputModifiers.LeftMouseButton;
+ if (state.HasFlag(ModifierType.Button2Mask))
+ rv |= InputModifiers.RightMouseButton;
+ if (state.HasFlag(ModifierType.Button3Mask))
+ rv |= InputModifiers.MiddleMouseButton;
+ return rv;
+ }
+
+ void OnButtonPressEvent(object o, Gtk.ButtonPressEventArgs args)
+ {
+ var evnt = args.Event;
+ var e = new RawMouseEventArgs(
+ GtkMouseDevice.Instance,
+ evnt.Time,
+ _inputRoot,
+ evnt.Button == 1
+ ? RawMouseEventType.LeftButtonDown
+ : evnt.Button == 3 ? RawMouseEventType.RightButtonDown : RawMouseEventType.MiddleButtonDown,
+ new Point(evnt.X, evnt.Y), GetModifierKeys(evnt.State));
+ Input(e);
+ }
+
+ void OnScrollEvent(object o, Gtk.ScrollEventArgs args)
+ {
+ var evnt = args.Event;
+ double step = 1;
+ var delta = new Vector();
+ if (evnt.Direction == ScrollDirection.Down)
+ delta = new Vector(0, -step);
+ else if (evnt.Direction == ScrollDirection.Up)
+ delta = new Vector(0, step);
+ else if (evnt.Direction == ScrollDirection.Right)
+ delta = new Vector(-step, 0);
+ if (evnt.Direction == ScrollDirection.Left)
+ delta = new Vector(step, 0);
+ var e = new RawMouseWheelEventArgs(GtkMouseDevice.Instance, evnt.Time, _inputRoot, new Point(evnt.X, evnt.Y), delta, GetModifierKeys(evnt.State));
+ Input(e);
+ }
+
+ protected void OnButtonReleaseEvent(object o, Gtk.ButtonReleaseEventArgs args)
+ {
+ var evnt = args.Event;
+ var e = new RawMouseEventArgs(
+ GtkMouseDevice.Instance,
+ evnt.Time,
+ _inputRoot,
+ evnt.Button == 1
+ ? RawMouseEventType.LeftButtonUp
+ : evnt.Button == 3 ? RawMouseEventType.RightButtonUp : RawMouseEventType.MiddleButtonUp,
+ new Point(evnt.X, evnt.Y), GetModifierKeys(evnt.State));
+ Input(e);
+ }
+
+ void OnDestroyed(object sender, EventArgs eventArgs)
+ {
+ Closed();
+ }
+
+ private void ProcessKeyEvent(EventKey evnt)
+ {
+
+ _lastKeyEventTimestamp = evnt.Time;
+ if (_imContext.FilterKeypress(evnt))
+ return;
+ var e = new RawKeyEventArgs(
+ GtkKeyboardDevice.Instance,
+ evnt.Time,
+ evnt.Type == EventType.KeyPress ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp,
+ GtkKeyboardDevice.ConvertKey(evnt.Key), GetModifierKeys(evnt.State));
+ Input(e);
+ }
+
+ void OnKeyPressEvent(object o, Gtk.KeyPressEventArgs args)
+ {
+ args.RetVal = true;
+ ProcessKeyEvent(args.Event);
+ }
+
+ void OnKeyReleaseEvent(object o, Gtk.KeyReleaseEventArgs args)
+ {
+ args.RetVal = true;
+ ProcessKeyEvent(args.Event);
+ }
+
+ private void ImContext_Commit(object o, Gtk.CommitArgs args)
+ {
+ Input(new RawTextInputEventArgs(GtkKeyboardDevice.Instance, _lastKeyEventTimestamp, args.Str));
+ }
+
+ void OnExposeEvent(object o, Gtk.ExposeEventArgs args)
+ {
+ Paint(args.Event.Area.ToAvalonia());
+ args.RetVal = true;
+ }
+
+ void OnMotionNotifyEvent(object o, Gtk.MotionNotifyEventArgs args)
+ {
+ var evnt = args.Event;
+ var position = new Point(evnt.X, evnt.Y);
+
+ GtkMouseDevice.Instance.SetClientPosition(position);
+
+ var e = new RawMouseEventArgs(
+ GtkMouseDevice.Instance,
+ evnt.Time,
+ _inputRoot,
+ RawMouseEventType.Move,
+ position, GetModifierKeys(evnt.State));
+ Input(e);
+ args.RetVal = true;
+ }
+
+ public void Dispose()
+ {
+ _window.Dispose();
+ }
+ }
+}
diff --git a/src/Gtk/Avalonia.Gtk/Windows.cs b/src/Gtk/Avalonia.Gtk/Windows.cs
new file mode 100644
index 0000000000..fe325f4114
--- /dev/null
+++ b/src/Gtk/Avalonia.Gtk/Windows.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading.Tasks;
+using Avalonia.Platform;
+using Gdk;
+using Gtk;
+using Window = Gtk.Window;
+using WindowType = Gtk.WindowType;
+
+namespace Avalonia.Gtk
+{
+ class PlatformHandleAwareWindow : Window, IPlatformHandle
+ {
+ public PlatformHandleAwareWindow(WindowType type) : base(type)
+ {
+ Events = EventMask.AllEventsMask;
+ }
+
+ IntPtr IPlatformHandle.Handle => GetNativeWindow();
+ public string HandleDescriptor => "HWND";
+
+
+ [DllImport("libgdk-win32-2.0-0.dll", CallingConvention = CallingConvention.Cdecl)]
+ static extern IntPtr gdk_win32_drawable_get_handle(IntPtr gdkWindow);
+
+ [DllImport("libgtk-x11-2.0.so.0", CallingConvention = CallingConvention.Cdecl)]
+ static extern IntPtr gdk_x11_drawable_get_xid(IntPtr gdkWindow);
+
+ [DllImport("libgdk-quartz-2.0-0.dylib", CallingConvention = CallingConvention.Cdecl)]
+ static extern IntPtr gdk_quartz_window_get_nswindow(IntPtr gdkWindow);
+
+ IntPtr _nativeWindow;
+
+ IntPtr GetNativeWindow()
+ {
+ if (_nativeWindow != IntPtr.Zero)
+ return _nativeWindow;
+ return _nativeWindow = GetNativeWindow(GdkWindow);
+ }
+
+ public static IntPtr GetNativeWindow(Gdk.Window window)
+ {
+ IntPtr h = window.Handle;
+
+ //Try whatever backend that works
+ try
+ {
+ return gdk_quartz_window_get_nswindow(h);
+ }
+ catch
+ {
+ }
+ try
+ {
+ return gdk_x11_drawable_get_xid(h);
+ }
+ catch
+ {
+ }
+ return gdk_win32_drawable_get_handle(h);
+ }
+
+ protected override bool OnConfigureEvent(EventConfigure evnt)
+ {
+ base.OnConfigureEvent(evnt);
+ return false;
+ }
+ }
+
+ class PlatformHandleAwareDrawingArea : DrawingArea, IPlatformHandle
+ {
+
+
+
+ IntPtr IPlatformHandle.Handle => GetNativeWindow();
+ public string HandleDescriptor => "HWND";
+ IntPtr _nativeWindow;
+
+ IntPtr GetNativeWindow()
+ {
+
+ if (_nativeWindow != IntPtr.Zero)
+ return _nativeWindow;
+ Realize();
+ return _nativeWindow = PlatformHandleAwareWindow.GetNativeWindow(GdkWindow);
+ }
+ }
+}
diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs b/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs
index ec60695374..086257f24c 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs
@@ -27,10 +27,12 @@ namespace Avalonia.Markup.Xaml.Data
/// Initializes a new instance of the class.
///
/// The binding path.
- public Binding(string path)
+ /// The binding mode.
+ public Binding(string path, BindingMode mode = BindingMode.Default)
: this()
{
Path = path;
+ Mode = mode;
}
///
@@ -78,21 +80,18 @@ namespace Avalonia.Markup.Xaml.Data
///
public object Source { get; set; }
- ///
- /// Gets or sets a value indicating whether the property should be validated.
- ///
- public bool EnableValidation { get; set; }
-
///
public InstancedBinding Initiate(
IAvaloniaObject target,
AvaloniaProperty targetProperty,
- object anchor = null)
+ object anchor = null,
+ bool enableDataValidation = false)
{
Contract.Requires(target != null);
var pathInfo = ParsePath(Path);
ValidateState(pathInfo);
+ enableDataValidation = enableDataValidation && Priority == BindingPriority.LocalValue;
ExpressionObserver observer;
@@ -105,7 +104,7 @@ namespace Avalonia.Markup.Xaml.Data
}
else if (Source != null)
{
- observer = CreateSourceObserver(Source, pathInfo.Path);
+ observer = CreateSourceObserver(Source, pathInfo.Path, enableDataValidation);
}
else if (RelativeSource == null || RelativeSource.Mode == RelativeSourceMode.DataContext)
{
@@ -113,7 +112,8 @@ namespace Avalonia.Markup.Xaml.Data
target,
pathInfo.Path,
targetProperty == Control.DataContextProperty,
- anchor);
+ anchor,
+ enableDataValidation);
}
else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent)
{
@@ -135,7 +135,7 @@ namespace Avalonia.Markup.Xaml.Data
fallback = null;
}
- var subject = new ExpressionSubject(
+ var subject = new BindingExpression(
observer,
targetProperty?.PropertyType ?? typeof(object),
fallback,
@@ -197,7 +197,8 @@ namespace Avalonia.Markup.Xaml.Data
IAvaloniaObject target,
string path,
bool targetIsDataContext,
- object anchor)
+ object anchor,
+ bool enableDataValidation)
{
Contract.Requires(target != null);
@@ -220,19 +221,16 @@ namespace Avalonia.Markup.Xaml.Data
() => target.GetValue(Control.DataContextProperty),
path,
update,
- EnableValidation);
+ enableDataValidation);
return result;
}
else
{
return new ExpressionObserver(
- target.GetObservable(Visual.VisualParentProperty)
- .OfType()
- .Select(x => x.GetObservable(Control.DataContextProperty))
- .Switch(),
+ GetParentDataContext(target),
path,
- EnableValidation);
+ enableDataValidation);
}
}
@@ -240,18 +238,23 @@ namespace Avalonia.Markup.Xaml.Data
{
Contract.Requires(target != null);
+ var description = $"#{elementName}.{path}";
var result = new ExpressionObserver(
ControlLocator.Track(target, elementName),
path,
- EnableValidation);
+ false,
+ description);
return result;
}
- private ExpressionObserver CreateSourceObserver(object source, string path)
+ private ExpressionObserver CreateSourceObserver(
+ object source,
+ string path,
+ bool enabledDataValidation)
{
Contract.Requires(source != null);
- return new ExpressionObserver(source, path, EnableValidation);
+ return new ExpressionObserver(source, path, enabledDataValidation);
}
private ExpressionObserver CreateTemplatedParentObserver(
@@ -272,6 +275,22 @@ namespace Avalonia.Markup.Xaml.Data
return result;
}
+ private IObservable GetParentDataContext(IAvaloniaObject target)
+ {
+ // The DataContext is based on the visual parent and not the logical parent: this may
+ // seem unintuitive considering the fact that property inheritance works on the logical
+ // tree, but consider a ContentControl with a ContentPresenter. The ContentControl's
+ // Content property is bound to a value which becomes the ContentPresenter's
+ // DataContext - it is from this that the child hosted by the ContentPresenter needs to
+ // inherit its DataContext.
+ return target.GetObservable(Visual.VisualParentProperty)
+ .Select(x =>
+ {
+ return (x as IAvaloniaObject)?.GetObservable(Control.DataContextProperty) ??
+ Observable.Return((object)null);
+ }).Switch();
+ }
+
private class PathInfo
{
public string Path { get; set; }
diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup.Xaml/Data/MultiBinding.cs
index 91a502a44f..69190be220 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Data/MultiBinding.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/Data/MultiBinding.cs
@@ -53,7 +53,8 @@ namespace Avalonia.Markup.Xaml.Data
public InstancedBinding Initiate(
IAvaloniaObject target,
AvaloniaProperty targetProperty,
- object anchor = null)
+ object anchor = null,
+ bool enableDataValidation = false)
{
if (Converter == null)
{
diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/StyleResourceBinding.cs b/src/Markup/Avalonia.Markup.Xaml/Data/StyleResourceBinding.cs
index 442f9199e8..c538c0768e 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Data/StyleResourceBinding.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/Data/StyleResourceBinding.cs
@@ -37,7 +37,8 @@ namespace Avalonia.Markup.Xaml.Data
public InstancedBinding Initiate(
IAvaloniaObject target,
AvaloniaProperty targetProperty,
- object anchor = null)
+ object anchor = null,
+ bool enableDataValidation = false)
{
if (Name == "Red")
{
diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
index 70d3f7d161..b131f07fd4 100644
--- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
@@ -29,7 +29,6 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
Mode = Mode,
Path = Path,
Priority = Priority,
- EnableValidation = EnableValidation,
};
}
@@ -41,6 +40,5 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
public string Path { get; set; }
public BindingPriority Priority { get; set; } = BindingPriority.LocalValue;
public object Source { get; set; }
- public bool EnableValidation { get; set; }
}
}
\ No newline at end of file
diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs
index be4287605c..5e412633d2 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs
@@ -5,14 +5,13 @@ using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Markup.Data;
using System;
+using System.Reactive.Linq;
namespace Avalonia.Markup.Xaml.Templates
{
public class MemberSelector : IMemberSelector
{
- private ExpressionNode _expressionNode;
private string _memberName;
- private ExpressionNode _memberValueNode;
public string MemberName
{
@@ -22,8 +21,6 @@ namespace Avalonia.Markup.Xaml.Templates
if (_memberName != value)
{
_memberName = value;
- _expressionNode = null;
- _memberValueNode = null;
}
}
}
@@ -35,34 +32,11 @@ namespace Avalonia.Markup.Xaml.Templates
return o;
}
- if (_expressionNode == null)
- {
- _expressionNode = ExpressionNodeBuilder.Build(MemberName);
-
- _memberValueNode = _expressionNode;
-
- while (_memberValueNode.Next != null)
- {
- _memberValueNode = _memberValueNode.Next;
- }
- }
-
- _expressionNode.Target = new WeakReference(o);
-
- object result = _memberValueNode.CurrentValue.Target;
-
- _expressionNode.Target = null;
-
- if (result == AvaloniaProperty.UnsetValue)
- {
- return null;
- }
- else if (result is BindingError)
- {
- return null;
- }
+ var expression = new ExpressionObserver(o, MemberName);
+ object result = AvaloniaProperty.UnsetValue;
- return result;
+ expression.Subscribe(x => result = x);
+ return (result == AvaloniaProperty.UnsetValue || result is BindingNotification) ? null : result;
}
}
}
\ No newline at end of file
diff --git a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj
index 49a5a4a681..738f381f4e 100644
--- a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj
+++ b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj
@@ -41,14 +41,17 @@
Properties\SharedAssemblyInfo.cs
+
+
-
+
+
-
+
@@ -59,8 +62,12 @@
+
+
+
+
-
+
diff --git a/src/Markup/Avalonia.Markup/Data/BindingExpression.cs b/src/Markup/Avalonia.Markup/Data/BindingExpression.cs
new file mode 100644
index 0000000000..0f4c091bff
--- /dev/null
+++ b/src/Markup/Avalonia.Markup/Data/BindingExpression.cs
@@ -0,0 +1,305 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Globalization;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+using Avalonia.Data;
+using Avalonia.Logging;
+using Avalonia.Utilities;
+
+namespace Avalonia.Markup.Data
+{
+ ///
+ /// Binds to an expression on an object using a type value converter to convert the values
+ /// that are send and received.
+ ///
+ public class BindingExpression : ISubject, IDescription
+ {
+ private readonly ExpressionObserver _inner;
+ private readonly Type _targetType;
+ private readonly object _fallbackValue;
+ private readonly BindingPriority _priority;
+ private readonly Subject _errors = new Subject();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The .
+ /// The type to convert the value to.
+ public BindingExpression(ExpressionObserver inner, Type targetType)
+ : this(inner, targetType, DefaultValueConverter.Instance)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The .
+ /// The type to convert the value to.
+ /// The value converter to use.
+ ///
+ /// A parameter to pass to .
+ ///
+ /// The binding priority.
+ public BindingExpression(
+ ExpressionObserver inner,
+ Type targetType,
+ IValueConverter converter,
+ object converterParameter = null,
+ BindingPriority priority = BindingPriority.LocalValue)
+ : this(inner, targetType, AvaloniaProperty.UnsetValue, converter, converterParameter, priority)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The .
+ /// The type to convert the value to.
+ ///
+ /// The value to use when the binding is unable to produce a value.
+ ///
+ /// The value converter to use.
+ ///
+ /// A parameter to pass to .
+ ///
+ /// The binding priority.
+ public BindingExpression(
+ ExpressionObserver inner,
+ Type targetType,
+ object fallbackValue,
+ IValueConverter converter,
+ object converterParameter = null,
+ BindingPriority priority = BindingPriority.LocalValue)
+ {
+ Contract.Requires(inner != null);
+ Contract.Requires(targetType != null);
+ Contract.Requires(converter != null);
+
+ _inner = inner;
+ _targetType = targetType;
+ Converter = converter;
+ ConverterParameter = converterParameter;
+ _fallbackValue = fallbackValue;
+ _priority = priority;
+ }
+
+ ///
+ /// Gets the converter to use on the expression.
+ ///
+ public IValueConverter Converter { get; }
+
+ ///
+ /// Gets a parameter to pass to .
+ ///
+ public object ConverterParameter { get; }
+
+ ///
+ string IDescription.Description => _inner.Expression;
+
+ ///
+ public void OnCompleted()
+ {
+ }
+
+ ///
+ public void OnError(Exception error)
+ {
+ }
+
+ ///
+ public void OnNext(object value)
+ {
+ using (_inner.Subscribe(_ => { }))
+ {
+ var type = _inner.ResultType;
+
+ if (type != null)
+ {
+ var converted = Converter.ConvertBack(
+ value,
+ type,
+ ConverterParameter,
+ CultureInfo.CurrentUICulture);
+
+ if (converted == AvaloniaProperty.UnsetValue)
+ {
+ converted = TypeUtilities.Default(type);
+ _inner.SetValue(converted, _priority);
+ }
+ else if (converted is BindingNotification)
+ {
+ var notification = converted as BindingNotification;
+
+ if (notification.ErrorType == BindingErrorType.None)
+ {
+ throw new AvaloniaInternalException(
+ "IValueConverter should not return non-errored BindingNotification.");
+ }
+
+ _errors.OnNext(notification);
+
+ if (_fallbackValue != AvaloniaProperty.UnsetValue)
+ {
+ if (TypeUtilities.TryConvert(
+ type,
+ _fallbackValue,
+ CultureInfo.InvariantCulture,
+ out converted))
+ {
+ _inner.SetValue(converted, _priority);
+ }
+ else
+ {
+ Logger.Error(
+ LogArea.Binding,
+ this,
+ "Could not convert FallbackValue {FallbackValue} to {Type}",
+ _fallbackValue,
+ type);
+ }
+ }
+ }
+ else
+ {
+ _inner.SetValue(converted, _priority);
+ }
+ }
+ }
+ }
+
+ ///
+ public IDisposable Subscribe(IObserver observer)
+ {
+ return _inner.Select(ConvertValue).Merge(_errors).Subscribe(observer);
+ }
+
+ private object ConvertValue(object value)
+ {
+ var notification = value as BindingNotification;
+
+ if (notification == null)
+ {
+ var converted = Converter.Convert(
+ value,
+ _targetType,
+ ConverterParameter,
+ CultureInfo.CurrentUICulture);
+
+ notification = converted as BindingNotification;
+
+ if (notification?.ErrorType == BindingErrorType.None)
+ {
+ converted = notification.Value;
+ }
+
+ if (_fallbackValue != AvaloniaProperty.UnsetValue &&
+ (converted == AvaloniaProperty.UnsetValue || converted is BindingNotification))
+ {
+ var fallback = ConvertFallback();
+ converted = Merge(converted, fallback);
+ }
+
+ return converted;
+ }
+ else
+ {
+ return ConvertValue(notification);
+ }
+ }
+
+ private BindingNotification ConvertValue(BindingNotification notification)
+ {
+ if (notification.HasValue)
+ {
+ var converted = ConvertValue(notification.Value);
+ notification = Merge(notification, converted);
+ }
+ else if (_fallbackValue != AvaloniaProperty.UnsetValue)
+ {
+ var fallback = ConvertFallback();
+ notification = Merge(notification, fallback);
+ }
+
+ return notification;
+ }
+
+ private BindingNotification ConvertFallback()
+ {
+ object converted;
+
+ if (_fallbackValue == AvaloniaProperty.UnsetValue)
+ {
+ throw new AvaloniaInternalException("Cannot call ConvertFallback with no fallback value");
+ }
+
+ if (TypeUtilities.TryConvert(
+ _targetType,
+ _fallbackValue,
+ CultureInfo.InvariantCulture,
+ out converted))
+ {
+ return new BindingNotification(converted);
+ }
+ else
+ {
+ return new BindingNotification(
+ new InvalidCastException(
+ $"Could not convert FallbackValue '{_fallbackValue}' to '{_targetType}'"),
+ BindingErrorType.Error);
+ }
+ }
+
+ private static BindingNotification Merge(object a, BindingNotification b)
+ {
+ var an = a as BindingNotification;
+
+ if (an != null)
+ {
+ Merge(an, b);
+ return an;
+ }
+ else
+ {
+ return b;
+ }
+ }
+
+ private static BindingNotification Merge(BindingNotification a, object b)
+ {
+ var bn = b as BindingNotification;
+
+ if (bn != null)
+ {
+ Merge(a, bn);
+ }
+ else
+ {
+ a.SetValue(b);
+ }
+
+ return a;
+ }
+
+ private static BindingNotification Merge(BindingNotification a, BindingNotification b)
+ {
+ if (b.HasValue)
+ {
+ a.SetValue(b.Value);
+ }
+ else
+ {
+ a.ClearValue();
+ }
+
+ if (b.Error != null)
+ {
+ a.AddError(b.Error, b.ErrorType);
+ }
+
+ return a;
+ }
+ }
+}
diff --git a/src/Markup/Avalonia.Markup/Data/EmptyExpressionNode.cs b/src/Markup/Avalonia.Markup/Data/EmptyExpressionNode.cs
new file mode 100644
index 0000000000..02ecd817da
--- /dev/null
+++ b/src/Markup/Avalonia.Markup/Data/EmptyExpressionNode.cs
@@ -0,0 +1,18 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Reactive.Linq;
+
+namespace Avalonia.Markup.Data
+{
+ internal class EmptyExpressionNode : ExpressionNode
+ {
+ public override string Description => ".";
+
+ protected override IObservable StartListeningCore(WeakReference reference)
+ {
+ return Observable.Return(reference.Target);
+ }
+ }
+}
diff --git a/src/Markup/Avalonia.Markup/Data/ExpressionNode.cs b/src/Markup/Avalonia.Markup/Data/ExpressionNode.cs
index 90a654d9e6..b0957c7187 100644
--- a/src/Markup/Avalonia.Markup/Data/ExpressionNode.cs
+++ b/src/Markup/Avalonia.Markup/Data/ExpressionNode.cs
@@ -2,124 +2,188 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
using System.Reactive.Subjects;
using Avalonia.Data;
namespace Avalonia.Markup.Data
{
- internal abstract class ExpressionNode : IObservable
+ internal abstract class ExpressionNode : ISubject
{
protected static readonly WeakReference UnsetReference =
new WeakReference(AvaloniaProperty.UnsetValue);
- private WeakReference _target;
-
- private Subject _subject;
-
- private WeakReference _value = UnsetReference;
+ private WeakReference _target = UnsetReference;
+ private IDisposable _valueSubscription;
+ private IObserver _observer;
+ private IDisposable _valuePluginSubscription;
+ public abstract string Description { get; }
public ExpressionNode Next { get; set; }
public WeakReference Target
{
- get
- {
- return _target;
- }
+ get { return _target; }
set
{
- var newInstance = value?.Target;
- var oldInstance = _target?.Target;
+ Contract.Requires(value != null);
- if (!object.Equals(oldInstance, newInstance))
- {
- if (oldInstance != null)
- {
- Unsubscribe(oldInstance);
- }
+ var oldTarget = _target?.Target;
+ var newTarget = value.Target;
+ var running = _valueSubscription != null;
+ if (!ReferenceEquals(oldTarget, newTarget))
+ {
+ _valueSubscription?.Dispose();
+ _valueSubscription = null;
+ _valuePluginSubscription?.Dispose();
_target = value;
- if (newInstance != null)
+ if (running)
{
- SubscribeAndUpdate(_target);
- }
- else
- {
- CurrentValue = UnsetReference;
- }
-
- if (Next != null)
- {
- Next.Target = _value;
+ _valueSubscription = StartListening();
}
}
}
}
- public WeakReference CurrentValue
+ public IDisposable Subscribe(IObserver observer)
{
- get
+ if (_observer != null)
{
- return _value;
+ throw new AvaloniaInternalException("ExpressionNode can only be subscribed once.");
}
- set
+ _observer = observer;
+ var nextSubscription = Next?.Subscribe(this);
+ _valueSubscription = StartListening();
+
+ return Disposable.Create(() =>
{
- _value = value;
+ _valueSubscription?.Dispose();
+ _valueSubscription = null;
+ _valuePluginSubscription?.Dispose();
+ _valuePluginSubscription = null;
+ nextSubscription?.Dispose();
+ _observer = null;
+ });
+ }
- if (Next != null)
- {
- Next.Target = value;
- }
+ void IObserver.OnCompleted()
+ {
+ throw new AvaloniaInternalException("ExpressionNode.OnCompleted should not be called.");
+ }
- _subject?.OnNext(value.Target);
- }
+ void IObserver.OnError(Exception error)
+ {
+ throw new AvaloniaInternalException("ExpressionNode.OnError should not be called.");
}
- public virtual bool SetValue(object value, BindingPriority priority)
+ void IObserver.OnNext(object value)
{
- return Next?.SetValue(value, priority) ?? false;
+ NextValueChanged(value);
}
- public virtual IDisposable Subscribe(IObserver observer)
+ protected virtual IObservable StartListeningCore(WeakReference reference)
{
- if (Next != null)
+ return Observable.Return(reference.Target);
+ }
+
+ protected virtual void NextValueChanged(object value)
+ {
+ var bindingBroken = BindingNotification.ExtractError(value) as MarkupBindingChainNullException;
+ bindingBroken?.AddNode(Description);
+ _observer.OnNext(value);
+ }
+
+ private IDisposable StartListening()
+ {
+ var target = _target.Target;
+ IObservable source;
+
+ if (target == null)
+ {
+ source = Observable.Return(TargetNullNotification());
+ }
+ else if (target == AvaloniaProperty.UnsetValue)
{
- return Next.Subscribe(observer);
+ source = Observable.Empty();
}
else
{
- if (_subject == null)
- {
- _subject = new Subject();
- }
-
- observer.OnNext(CurrentValue.Target);
- return _subject.Subscribe(observer);
+ source = StartListeningCore(_target);
}
- }
- protected virtual void SubscribeAndUpdate(WeakReference reference)
- {
- CurrentValue = reference;
+ return source.Subscribe(TargetValueChanged);
}
- protected virtual void SendValidationStatus(IValidationStatus status)
+ private void TargetValueChanged(object value)
{
- //Even if elements only bound to sub-values, send validation changes along so they will be surfaced to the UI level.
- if (_subject != null)
+ var notification = value as BindingNotification;
+
+ if (notification == null)
{
- _subject.OnNext(status);
+ if (!HandleSpecialValue(value))
+ {
+ if (Next != null)
+ {
+ Next.Target = new WeakReference(value);
+ }
+ else
+ {
+ _observer.OnNext(value);
+ }
+ }
}
else
{
- Next?.SendValidationStatus(status);
+ if (notification.Error != null)
+ {
+ _observer.OnNext(notification);
+ }
+ else if (notification.HasValue)
+ {
+ if (!HandleSpecialValue(notification.Value))
+ {
+ if (Next != null)
+ {
+ Next.Target = new WeakReference(notification.Value);
+ }
+ else
+ {
+ _observer.OnNext(value);
+ }
+ }
+ }
+ }
+ }
+
+ private bool HandleSpecialValue(object value)
+ {
+ if (_valuePluginSubscription == null)
+ {
+ var reference = new WeakReference(value);
+
+ foreach (var plugin in ExpressionObserver.ValueHandlers)
+ {
+ if (plugin.Match(reference))
+ {
+ _valuePluginSubscription = plugin.Start(reference)?.Subscribe(TargetValueChanged);
+ return true;
+ }
+ }
}
+
+ return false;
}
- protected virtual void Unsubscribe(object target)
+ private BindingNotification TargetNullNotification()
{
+ return new BindingNotification(
+ new MarkupBindingChainNullException(),
+ BindingErrorType.Error,
+ AvaloniaProperty.UnsetValue);
}
}
}
diff --git a/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs b/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs
index 0b5fbc6d7d..819949b7b9 100644
--- a/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs
+++ b/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
+using System.Reactive.Subjects;
using Avalonia.Data;
using Avalonia.Markup.Data.Plugins;
@@ -31,41 +32,57 @@ namespace Avalonia.Markup.Data
/// An ordered collection of validation checker plugins that can be used to customize
/// the validation of view model and model data.
///
- public static readonly IList ValidationCheckers =
- new List
+ public static readonly IList DataValidators =
+ new List
{
+ new DataAnnotationsValidationPlugin(),
new IndeiValidationPlugin(),
+ new ExceptionValidationPlugin(),
};
- private readonly WeakReference _root;
- private readonly Func _rootGetter;
- private readonly IObservable _rootObservable;
- private readonly IObservable _update;
- private IDisposable _rootObserverSubscription;
- private IDisposable _updateSubscription;
- private int _count;
+ ///
+ /// An ordered collection of value handlers that can be used to customize the handling
+ /// of certain values.
+ ///
+ public static readonly IList ValueHandlers =
+ new List
+ {
+ new TaskValuePlugin(),
+ new ObservableValuePlugin(),
+ };
+
+ private static readonly object UninitializedValue = new object();
private readonly ExpressionNode _node;
- private bool _enableValidation;
+ private readonly Subject _finished;
+ private readonly object _root;
+ private IObservable _result;
///
/// Initializes a new instance of the class.
///
/// The root object.
/// The expression.
- /// Whether property validation should be enabled.
- public ExpressionObserver(object root, string expression, bool enableValidation = false)
+ /// Whether data validation should be enabled.
+ ///
+ /// A description of the expression. If null, will be used.
+ ///
+ public ExpressionObserver(
+ object root,
+ string expression,
+ bool enableDataValidation = false,
+ string description = null)
{
Contract.Requires(expression != null);
- _root = new WeakReference(root);
- _enableValidation = enableValidation;
-
- if (!string.IsNullOrWhiteSpace(expression))
+ if (root == AvaloniaProperty.UnsetValue)
{
- _node = ExpressionNodeBuilder.Build(expression, enableValidation);
+ root = null;
}
Expression = expression;
+ Description = description ?? expression;
+ _node = Parse(expression, enableDataValidation);
+ _root = new WeakReference(root);
}
///
@@ -73,24 +90,24 @@ namespace Avalonia.Markup.Data
///
/// An observable which provides the root object.
/// The expression.
- /// Whether property validation should be enabled.
+ /// Whether data validation should be enabled.
+ ///
+ /// A description of the expression. If null, will be used.
+ ///
public ExpressionObserver(
IObservable rootObservable,
string expression,
- bool enableValidation = false)
+ bool enableDataValidation = false,
+ string description = null)
{
Contract.Requires(rootObservable != null);
Contract.Requires(expression != null);
- _rootObservable = rootObservable;
- _enableValidation = enableValidation;
-
- if (!string.IsNullOrWhiteSpace(expression))
- {
- _node = ExpressionNodeBuilder.Build(expression, enableValidation);
- }
-
Expression = expression;
+ Description = description ?? expression;
+ _node = Parse(expression, enableDataValidation);
+ _finished = new Subject();
+ _root = rootObservable;
}
///
@@ -99,27 +116,28 @@ namespace Avalonia.Markup.Data
/// A function which gets the root object.
/// The expression.
/// An observable which triggers a re-read of the getter.
- /// Whether property validation should be enabled.
+ /// Whether data validation should be enabled.
+ ///
+ /// A description of the expression. If null, will be used.
+ ///
public ExpressionObserver(
Func rootGetter,
string expression,
IObservable update,
- bool enableValidation = false)
+ bool enableDataValidation = false,
+ string description = null)
{
Contract.Requires(rootGetter != null);
Contract.Requires(expression != null);
Contract.Requires(update != null);
- _rootGetter = rootGetter;
- _update = update;
- _enableValidation = enableValidation;
-
- if (!string.IsNullOrWhiteSpace(expression))
- {
- _node = ExpressionNodeBuilder.Build(expression, enableValidation);
- }
-
Expression = expression;
+ Description = description ?? expression;
+ _node = Parse(expression, enableDataValidation);
+ _finished = new Subject();
+
+ _node.Target = new WeakReference(rootGetter());
+ _root = update.Select(x => rootGetter());
}
///
@@ -129,27 +147,20 @@ namespace Avalonia.Markup.Data
/// The binding priority to use.
///
/// True if the value could be set; false if the expression does not evaluate to a
- /// property.
+ /// property. Note that the must be subscribed to
+ /// before setting the target value can work, as setting the value requires the
+ /// expression to be evaluated.
///
public bool SetValue(object value, BindingPriority priority = BindingPriority.LocalValue)
{
- IncrementCount();
-
- if (_rootGetter != null && _node != null)
- {
- _node.Target = new WeakReference(_rootGetter());
- }
-
- try
- {
- return _node?.SetValue(value, priority) ?? false;
- }
- finally
- {
- DecrementCount();
- }
+ return (Leaf as PropertyAccessorNode)?.SetTargetValue(value, priority) ?? false;
}
+ ///
+ /// Gets a description of the expression being observed.
+ ///
+ public string Description { get; }
+
///
/// Gets the expression being observed.
///
@@ -159,41 +170,7 @@ namespace Avalonia.Markup.Data
/// Gets the type of the expression result or null if the expression could not be
/// evaluated.
///
- public Type ResultType
- {
- get
- {
- IncrementCount();
-
- try
- {
- if (_node != null)
- {
- return (Leaf as PropertyAccessorNode)?.PropertyType;
- }
- else if (_rootGetter != null)
- {
- return _rootGetter()?.GetType();
- }
- else
- {
- return _root.Target?.GetType();
- }
- }
- finally
- {
- DecrementCount();
- }
- }
- }
-
- ///
- string IDescription.Description => Expression;
-
- ///
- /// Gets the root expression node. Used for testing.
- ///
- internal ExpressionNode Node => _node;
+ public Type ResultType => (Leaf as PropertyAccessorNode)?.PropertyType;
///
/// Gets the leaf node.
@@ -211,94 +188,88 @@ namespace Avalonia.Markup.Data
///
protected override IDisposable SubscribeCore(IObserver observer)
{
- IncrementCount();
-
- if (_node != null)
+ if (_result == null)
{
- IObservable source = _node;
+ var source = (IObservable)_node;
- if (_rootObservable != null)
- {
- source = source.TakeUntil(_rootObservable.LastOrDefaultAsync());
- }
- else if (_update != null)
+ if (_finished != null)
{
- source = source.TakeUntil(_update.LastOrDefaultAsync());
+ source = source.TakeUntil(_finished);
}
- var subscription = source.Subscribe(observer);
-
- return Disposable.Create(() =>
- {
- DecrementCount();
- subscription.Dispose();
- });
+ _result = Observable.Using(StartRoot, _ => source)
+ .Select(ToWeakReference)
+ .Publish(UninitializedValue)
+ .RefCount()
+ .Where(x => x != UninitializedValue)
+ .Select(Translate);
}
- else if (_rootObservable != null)
+
+ return _result.Subscribe(observer);
+ }
+
+ private static ExpressionNode Parse(string expression, bool enableDataValidation)
+ {
+ if (!string.IsNullOrWhiteSpace(expression))
{
- return _rootObservable.Subscribe(observer);
+ return ExpressionNodeBuilder.Build(expression, enableDataValidation);
}
else
{
- if (_update == null)
- {
- return Observable.Never()
- .StartWith(_root.Target)
- .Subscribe(observer);
- }
- else
- {
- return _update
- .Select(_ => _rootGetter())
- .StartWith(_rootGetter())
- .Subscribe(observer);
- }
+ return new EmptyExpressionNode();
}
}
- private void IncrementCount()
+ private static object ToWeakReference(object o)
+ {
+ return o is BindingNotification ? o : new WeakReference(o);
+ }
+
+ private object Translate(object o)
{
- if (_count++ == 0 && _node != null)
+ var weak = o as WeakReference;
+
+ if (weak != null)
{
- if (_rootGetter != null)
- {
- _node.Target = new WeakReference(_rootGetter());
+ return weak.Target;
+ }
+ else
+ {
+ var broken = BindingNotification.ExtractError(o) as MarkupBindingChainNullException;
- if (_update != null)
+ if (broken != null)
+ {
+ // We've received notification of a broken expression due to a null value
+ // somewhere in the chain. If this null value occurs at the first node then we
+ // ignore it, as its likely that e.g. the DataContext has not yet been set up.
+ if (broken.HasNodes)
{
- _updateSubscription = _update.Subscribe(x =>
- _node.Target = new WeakReference(_rootGetter()));
+ broken.Commit(Description);
+ }
+ else
+ {
+ o = AvaloniaProperty.UnsetValue;
}
}
- else if (_rootObservable != null)
- {
- _rootObserverSubscription = _rootObservable.Subscribe(x =>
- _node.Target = new WeakReference(x));
- }
- else
- {
- _node.Target = _root;
- }
+ return o;
}
}
- private void DecrementCount()
+ private IDisposable StartRoot()
{
- if (--_count == 0 && _node != null)
- {
- if (_rootObserverSubscription != null)
- {
- _rootObserverSubscription.Dispose();
- _rootObserverSubscription = null;
- }
+ var observable = _root as IObservable;
- if (_updateSubscription != null)
- {
- _updateSubscription.Dispose();
- _updateSubscription = null;
- }
-
- _node.Target = null;
+ if (observable != null)
+ {
+ return observable.Subscribe(
+ x => _node.Target = new WeakReference(x != AvaloniaProperty.UnsetValue ? x : null),
+ _ => _finished.OnNext(Unit.Default),
+ () => _finished.OnNext(Unit.Default));
+ }
+ else
+ {
+ _node.Target = (WeakReference)_root;
+ return Disposable.Empty;
}
}
}
diff --git a/src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs b/src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs
deleted file mode 100644
index 0a3be26c18..0000000000
--- a/src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs
+++ /dev/null
@@ -1,213 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.Globalization;
-using System.Reactive.Linq;
-using System.Reactive.Subjects;
-using Avalonia.Data;
-using Avalonia.Logging;
-using Avalonia.Utilities;
-
-namespace Avalonia.Markup.Data
-{
- ///
- /// Turns an into a subject that can be bound two-way with
- /// a value converter.
- ///
- public class ExpressionSubject : ISubject, IDescription
- {
- private readonly ExpressionObserver _inner;
- private readonly Type _targetType;
- private readonly object _fallbackValue;
- private readonly BindingPriority _priority;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The .
- /// The type to convert the value to.
- public ExpressionSubject(ExpressionObserver inner, Type targetType)
- : this(inner, targetType, DefaultValueConverter.Instance)
- {
- }
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The .
- /// The type to convert the value to.
- /// The value converter to use.
- ///
- /// A parameter to pass to .
- ///
- /// The binding priority.
- public ExpressionSubject(
- ExpressionObserver inner,
- Type targetType,
- IValueConverter converter,
- object converterParameter = null,
- BindingPriority priority = BindingPriority.LocalValue)
- : this(inner, targetType, AvaloniaProperty.UnsetValue, converter, converterParameter, priority)
- {
- }
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The .
- /// The type to convert the value to.
- ///
- /// The value to use when the binding is unable to produce a value.
- ///
- /// The value converter to use.
- ///
- /// A parameter to pass to .
- ///
- /// The binding priority.
- public ExpressionSubject(
- ExpressionObserver inner,
- Type targetType,
- object fallbackValue,
- IValueConverter converter,
- object converterParameter = null,
- BindingPriority priority = BindingPriority.LocalValue)
- {
- Contract.Requires(inner != null);
- Contract.Requires(targetType != null);
- Contract.Requires(converter != null);
-
- _inner = inner;
- _targetType = targetType;
- Converter = converter;
- ConverterParameter = converterParameter;
- _fallbackValue = fallbackValue;
- _priority = priority;
- }
-
- ///
- /// Gets the converter to use on the expression.
- ///
- public IValueConverter Converter { get; }
-
- ///
- /// Gets a parameter to pass to .
- ///
- public object ConverterParameter { get; }
-
- ///
- string IDescription.Description => _inner.Expression;
-
- ///
- public void OnCompleted()
- {
- }
-
- ///
- public void OnError(Exception error)
- {
- }
-
- ///
- public void OnNext(object value)
- {
- var type = _inner.ResultType;
-
- if (type != null)
- {
- var converted = Converter.ConvertBack(
- value,
- type,
- ConverterParameter,
- CultureInfo.CurrentUICulture);
-
- if (converted == AvaloniaProperty.UnsetValue)
- {
- converted = TypeUtilities.Default(type);
- _inner.SetValue(converted, _priority);
- }
- else if (converted is BindingError)
- {
- var error = converted as BindingError;
-
- Logger.Error(
- LogArea.Binding,
- this,
- "Error binding to {Expression}: {Message}",
- _inner.Expression,
- error.Exception.Message);
-
- if (_fallbackValue != AvaloniaProperty.UnsetValue)
- {
- if (TypeUtilities.TryConvert(
- type,
- _fallbackValue,
- CultureInfo.InvariantCulture,
- out converted))
- {
- _inner.SetValue(converted, _priority);
- }
- else
- {
- Logger.Error(
- LogArea.Binding,
- this,
- "Could not convert FallbackValue {FallbackValue} to {Type}",
- _fallbackValue,
- type);
- }
- }
- }
- else
- {
- _inner.SetValue(converted, _priority);
- }
- }
- }
-
- ///
- public IDisposable Subscribe(IObserver observer)
- {
- return _inner.Select(ConvertValue).Subscribe(observer);
- }
-
- private object ConvertValue(object value)
- {
- var converted =
- value as BindingError ??
- value as IValidationStatus ??
- Converter.Convert(
- value,
- _targetType,
- ConverterParameter,
- CultureInfo.CurrentUICulture);
-
- if (_fallbackValue != AvaloniaProperty.UnsetValue &&
- (converted == AvaloniaProperty.UnsetValue ||
- converted is BindingError))
- {
- var error = converted as BindingError;
-
- if (TypeUtilities.TryConvert(
- _targetType,
- _fallbackValue,
- CultureInfo.InvariantCulture,
- out converted))
- {
- if (error != null)
- {
- converted = new BindingError(error.Exception, converted);
- }
- }
- else
- {
- converted = new BindingError(
- new InvalidCastException(
- $"Could not convert FallbackValue '{_fallbackValue}' to '{_targetType}'"));
- }
- }
-
- return converted;
- }
- }
-}
diff --git a/src/Markup/Avalonia.Markup/Data/IndexerNode.cs b/src/Markup/Avalonia.Markup/Data/IndexerNode.cs
index 8849e7edbc..0a6b93bad1 100644
--- a/src/Markup/Avalonia.Markup/Data/IndexerNode.cs
+++ b/src/Markup/Avalonia.Markup/Data/IndexerNode.cs
@@ -10,130 +10,49 @@ using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Reflection;
+using System.Reactive.Linq;
namespace Avalonia.Markup.Data
{
- internal class IndexerNode : ExpressionNode,
- IWeakSubscriber,
- IWeakSubscriber
+ internal class IndexerNode : ExpressionNode
{
public IndexerNode(IList arguments)
{
Arguments = arguments;
}
- public IList Arguments { get; }
+ public override string Description => "[" + string.Join(",", Arguments) + "]";
- void IWeakSubscriber.OnEvent(object sender, NotifyCollectionChangedEventArgs e)
+ protected override IObservable StartListeningCore(WeakReference reference)
{
- var update = false;
- if (sender is IList)
- {
- object indexObject;
- if (!TypeUtilities.TryConvert(typeof(int), Arguments[0], CultureInfo.InvariantCulture, out indexObject))
- {
- return;
- }
- var index = (int)indexObject;
- switch (e.Action)
- {
- case NotifyCollectionChangedAction.Add:
- update = index >= e.NewStartingIndex;
- break;
- case NotifyCollectionChangedAction.Remove:
- update = index >= e.OldStartingIndex;
- break;
- case NotifyCollectionChangedAction.Replace:
- update = index >= e.NewStartingIndex &&
- index < e.NewStartingIndex + e.NewItems.Count;
- break;
- case NotifyCollectionChangedAction.Move:
- update = (index >= e.NewStartingIndex &&
- index < e.NewStartingIndex + e.NewItems.Count) ||
- (index >= e.OldStartingIndex &&
- index < e.OldStartingIndex + e.OldItems.Count);
- break;
- case NotifyCollectionChangedAction.Reset:
- update = true;
- break;
- }
- }
- else
- {
- update = true;
- }
-
- if (update)
- {
- CurrentValue = new WeakReference(GetValue(sender));
- }
- }
-
- void IWeakSubscriber.OnEvent(object sender, PropertyChangedEventArgs e)
- {
- var typeInfo = sender.GetType().GetTypeInfo();
-
- if (typeInfo.GetDeclaredProperty(e.PropertyName) == null)
- {
- return;
- }
-
- if (typeInfo.GetDeclaredProperty(e.PropertyName).GetIndexParameters().Any())
- {
- CurrentValue = new WeakReference(GetValue(sender));
- }
- }
-
- protected override void SubscribeAndUpdate(WeakReference reference)
- {
- object target = reference.Target;
-
- CurrentValue = new WeakReference(GetValue(target));
-
+ var target = reference.Target;
var incc = target as INotifyCollectionChanged;
-
- if (incc != null)
- {
- WeakSubscriptionManager.Subscribe(
- incc,
- nameof(incc.CollectionChanged),
- this);
- }
-
var inpc = target as INotifyPropertyChanged;
-
- if (inpc != null)
- {
- WeakSubscriptionManager.Subscribe(
- inpc,
- nameof(inpc.PropertyChanged),
- this);
- }
- }
-
- protected override void Unsubscribe(object target)
- {
- var incc = target as INotifyCollectionChanged;
+ var inputs = new List>();
if (incc != null)
{
- WeakSubscriptionManager.Unsubscribe(
- incc,
- nameof(incc.CollectionChanged),
- this);
+ inputs.Add(WeakObservable.FromEventPattern(
+ target,
+ nameof(incc.CollectionChanged))
+ .Where(x => ShouldUpdate(x.Sender, x.EventArgs))
+ .Select(_ => GetValue(target)));
}
- var inpc = target as INotifyPropertyChanged;
-
if (inpc != null)
{
- WeakSubscriptionManager.Unsubscribe(
- inpc,
- nameof(inpc.PropertyChanged),
- this);
+ inputs.Add(WeakObservable.FromEventPattern(
+ target,
+ nameof(inpc.PropertyChanged))
+ .Where(x => ShouldUpdate(x.Sender, x.EventArgs))
+ .Select(_ => GetValue(target)));
}
+
+ return Observable.Merge(inputs).StartWith(GetValue(target));
}
+ public IList Arguments { get; }
+
private object GetValue(object target)
{
var typeInfo = target.GetType().GetTypeInfo();
@@ -141,18 +60,23 @@ namespace Avalonia.Markup.Data
var dictionary = target as IDictionary;
var indexerProperty = GetIndexer(typeInfo);
var indexerParameters = indexerProperty?.GetIndexParameters();
+
if (indexerProperty != null && indexerParameters.Length == Arguments.Count)
{
var convertedObjectArray = new object[indexerParameters.Length];
+
for (int i = 0; i < Arguments.Count; i++)
{
object temp = null;
+
if (!TypeUtilities.TryConvert(indexerParameters[i].ParameterType, Arguments[i], CultureInfo.InvariantCulture, out temp))
{
return AvaloniaProperty.UnsetValue;
}
+
convertedObjectArray[i] = temp;
}
+
var intArgs = convertedObjectArray.OfType().ToArray();
// Try special cases where we can validate indicies
@@ -166,16 +90,18 @@ namespace Avalonia.Markup.Data
{
if (intArgs.Length == Arguments.Count && intArgs[0] >= 0 && intArgs[0] < list.Count)
{
- return list[intArgs[0]];
+ return list[intArgs[0]];
}
+
return AvaloniaProperty.UnsetValue;
}
else if (dictionary != null)
{
if (dictionary.Contains(convertedObjectArray[0]))
{
- return dictionary[convertedObjectArray[0]];
+ return dictionary[convertedObjectArray[0]];
}
+
return AvaloniaProperty.UnsetValue;
}
else
@@ -187,11 +113,11 @@ namespace Avalonia.Markup.Data
else
{
// Fallback to unchecked access
- return indexerProperty.GetValue(target, convertedObjectArray);
+ return indexerProperty.GetValue(target, convertedObjectArray);
}
}
// Multidimensional arrays end up here because the indexer search picks up the IList indexer instead of the
- // multidimensional indexer, which doesn't take the same number of arguments
+ // multidimensional indexer, which doesn't take the same number of arguments
else if (typeInfo.IsArray)
{
return GetValueFromArray((Array)target);
@@ -220,13 +146,16 @@ namespace Avalonia.Markup.Data
private bool ConvertArgumentsToInts(out int[] intArgs)
{
intArgs = new int[Arguments.Count];
+
for (int i = 0; i < Arguments.Count; ++i)
{
object value;
+
if (!TypeUtilities.TryConvert(typeof(int), Arguments[i], CultureInfo.InvariantCulture, out value))
{
return false;
}
+
intArgs[i] = (int)value;
}
return true;
@@ -235,7 +164,8 @@ namespace Avalonia.Markup.Data
private static PropertyInfo GetIndexer(TypeInfo typeInfo)
{
PropertyInfo indexer;
- for (;typeInfo != null; typeInfo = typeInfo.BaseType?.GetTypeInfo())
+
+ for (; typeInfo != null; typeInfo = typeInfo.BaseType?.GetTypeInfo())
{
// Check for the default indexer name first to make this faster.
// This will only be false when a class in VB has a custom indexer name.
@@ -243,14 +173,16 @@ namespace Avalonia.Markup.Data
{
return indexer;
}
+
foreach (var property in typeInfo.DeclaredProperties)
{
if (property.GetIndexParameters().Any())
{
return property;
}
- }
+ }
}
+
return null;
}
@@ -273,5 +205,46 @@ namespace Avalonia.Markup.Data
return false;
}
}
+
+ private bool ShouldUpdate(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ if (sender is IList)
+ {
+ object indexObject;
+
+ if (!TypeUtilities.TryConvert(typeof(int), Arguments[0], CultureInfo.InvariantCulture, out indexObject))
+ {
+ return false;
+ }
+
+ var index = (int)indexObject;
+
+ switch (e.Action)
+ {
+ case NotifyCollectionChangedAction.Add:
+ return index >= e.NewStartingIndex;
+ case NotifyCollectionChangedAction.Remove:
+ return index >= e.OldStartingIndex;
+ case NotifyCollectionChangedAction.Replace:
+ return index >= e.NewStartingIndex &&
+ index < e.NewStartingIndex + e.NewItems.Count;
+ case NotifyCollectionChangedAction.Move:
+ return (index >= e.NewStartingIndex &&
+ index < e.NewStartingIndex + e.NewItems.Count) ||
+ (index >= e.OldStartingIndex &&
+ index < e.OldStartingIndex + e.OldItems.Count);
+ case NotifyCollectionChangedAction.Reset:
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private bool ShouldUpdate(object sender, PropertyChangedEventArgs e)
+ {
+ var typeInfo = sender.GetType().GetTypeInfo();
+ return typeInfo.GetDeclaredProperty(e.PropertyName)?.GetIndexParameters().Any() ?? false;
+ }
}
}
diff --git a/src/Markup/Avalonia.Markup/Data/LogicalNotNode.cs b/src/Markup/Avalonia.Markup/Data/LogicalNotNode.cs
index 29fa842439..58a7915254 100644
--- a/src/Markup/Avalonia.Markup/Data/LogicalNotNode.cs
+++ b/src/Markup/Avalonia.Markup/Data/LogicalNotNode.cs
@@ -3,21 +3,17 @@
using System;
using System.Globalization;
-using System.Reactive.Linq;
using Avalonia.Data;
namespace Avalonia.Markup.Data
{
internal class LogicalNotNode : ExpressionNode
{
- public override bool SetValue(object value, BindingPriority priority)
- {
- return false;
- }
+ public override string Description => "!";
- public override IDisposable Subscribe(IObserver observer)
+ protected override void NextValueChanged(object value)
{
- return Next.Select(Negate).Subscribe(observer);
+ base.NextValueChanged(Negate(value));
}
private static object Negate(object v)
@@ -34,6 +30,12 @@ namespace Avalonia.Markup.Data
{
return !result;
}
+ else
+ {
+ return new BindingNotification(
+ new InvalidCastException($"Unable to convert '{s}' to bool."),
+ BindingErrorType.Error);
+ }
}
else
{
@@ -42,9 +44,17 @@ namespace Avalonia.Markup.Data
var boolean = Convert.ToBoolean(v, CultureInfo.InvariantCulture);
return !boolean;
}
- catch
+ catch (InvalidCastException)
+ {
+ // The error message here is "Unable to cast object of type 'System.Object'
+ // to type 'System.IConvertible'" which is kinda useless so provide our own.
+ return new BindingNotification(
+ new InvalidCastException($"Unable to convert '{v}' to bool."),
+ BindingErrorType.Error);
+ }
+ catch (Exception e)
{
- // TODO: Maybe should log something here.
+ return new BindingNotification(e, BindingErrorType.Error);
}
}
}
diff --git a/src/Markup/Avalonia.Markup/Data/MarkupBindingChainNullException.cs b/src/Markup/Avalonia.Markup/Data/MarkupBindingChainNullException.cs
new file mode 100644
index 0000000000..a549d6ebb6
--- /dev/null
+++ b/src/Markup/Avalonia.Markup/Data/MarkupBindingChainNullException.cs
@@ -0,0 +1,33 @@
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Data;
+
+namespace Avalonia.Markup.Data
+{
+ internal class MarkupBindingChainNullException : BindingChainNullException
+ {
+ private IList _nodes = new List();
+
+ public MarkupBindingChainNullException()
+ {
+ }
+
+ public MarkupBindingChainNullException(string expression, string expressionNullPoint)
+ : base(expression, expressionNullPoint)
+ {
+ _nodes = null;
+ }
+
+ public bool HasNodes => _nodes.Count > 0;
+ public void AddNode(string node) => _nodes.Add(node);
+
+ public void Commit(string expression)
+ {
+ Expression = expression;
+ ExpressionNullPoint = string.Join(".", _nodes.Reverse())
+ .Replace(".!", "!")
+ .Replace(".[", "[");
+ _nodes = null;
+ }
+ }
+}
diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/AvaloniaPropertyAccessorPlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/AvaloniaPropertyAccessorPlugin.cs
index b2d73ab8fc..1d4a9a688e 100644
--- a/src/Markup/Avalonia.Markup/Data/Plugins/AvaloniaPropertyAccessorPlugin.cs
+++ b/src/Markup/Avalonia.Markup/Data/Plugins/AvaloniaPropertyAccessorPlugin.cs
@@ -4,7 +4,6 @@
using System;
using System.Reactive.Linq;
using Avalonia.Data;
-using Avalonia.Logging;
namespace Avalonia.Markup.Data.Plugins
{
@@ -13,36 +12,22 @@ namespace Avalonia.Markup.Data.Plugins
///
public class AvaloniaPropertyAccessorPlugin : IPropertyAccessorPlugin
{
- ///
- /// Checks whether this plugin can handle accessing the properties of the specified object.
- ///
- /// A weak reference to the object.
- /// True if the plugin can handle the object; otherwise false.
- public bool Match(WeakReference reference)
- {
- Contract.Requires(reference != null);
-
- return reference.Target is AvaloniaObject;
- }
+ ///
+ public bool Match(WeakReference reference) => reference.Target is AvaloniaObject;
///
/// Starts monitoring the value of a property on an object.
///
/// A weak reference to the object.
/// The property name.
- /// A function to call when the property changes.
///
/// An interface through which future interactions with the
/// property will be made.
///
- public IPropertyAccessor Start(
- WeakReference reference,
- string propertyName,
- Action changed)
+ public IPropertyAccessor Start(WeakReference reference, string propertyName)
{
Contract.Requires(reference != null);
Contract.Requires(propertyName != null);
- Contract.Requires(changed != null);
var instance = reference.Target;
var o = (AvaloniaObject)instance;
@@ -50,13 +35,13 @@ namespace Avalonia.Markup.Data.Plugins
if (p != null)
{
- return new Accessor(new WeakReference(o), p, changed);
+ return new Accessor(new WeakReference(o), p);
}
else if (instance != AvaloniaProperty.UnsetValue)
{
var message = $"Could not find AvaloniaProperty '{propertyName}' on '{instance}'";
var exception = new MissingMemberException(message);
- return new PropertyError(new BindingError(exception));
+ return new PropertyError(new BindingNotification(exception, BindingErrorType.Error));
}
else
{
@@ -64,23 +49,19 @@ namespace Avalonia.Markup.Data.Plugins
}
}
- private class Accessor : IPropertyAccessor
+ private class Accessor : PropertyAccessorBase
{
private readonly WeakReference _reference;
private readonly AvaloniaProperty _property;
private IDisposable _subscription;
- public Accessor(
- WeakReference reference,
- AvaloniaProperty property,
- Action changed)
+ public Accessor(WeakReference reference, AvaloniaProperty property)
{
Contract.Requires(reference != null);
Contract.Requires(property != null);
_reference = reference;
_property = property;
- _subscription = Instance.GetWeakObservable(property).Skip(1).Subscribe(changed);
}
public AvaloniaObject Instance
@@ -93,17 +74,10 @@ namespace Avalonia.Markup.Data.Plugins
}
}
- public Type PropertyType => _property.PropertyType;
-
- public object Value => Instance.GetValue(_property);
+ public override Type PropertyType => _property.PropertyType;
+ public override object Value => Instance?.GetValue(_property);
- public void Dispose()
- {
- _subscription?.Dispose();
- _subscription = null;
- }
-
- public bool SetValue(object value, BindingPriority priority)
+ public override bool SetValue(object value, BindingPriority priority)
{
if (!_property.IsReadOnly)
{
@@ -113,6 +87,17 @@ namespace Avalonia.Markup.Data.Plugins
return false;
}
+
+ protected override void Dispose(bool disposing)
+ {
+ _subscription?.Dispose();
+ _subscription = null;
+ }
+
+ protected override void SubscribeCore(IObserver observer)
+ {
+ _subscription = Instance.GetWeakObservable(_property).Subscribe(observer);
+ }
}
}
}
diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/DataAnnotationsValidationPlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/DataAnnotationsValidationPlugin.cs
new file mode 100644
index 0000000000..859438636a
--- /dev/null
+++ b/src/Markup/Avalonia.Markup/Data/Plugins/DataAnnotationsValidationPlugin.cs
@@ -0,0 +1,81 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Reflection;
+using Avalonia.Data;
+
+namespace Avalonia.Markup.Data.Plugins
+{
+ ///
+ /// Validates properties on that have s.
+ ///
+ public class DataAnnotationsValidationPlugin : IDataValidationPlugin
+ {
+ ///
+ public bool Match(WeakReference reference, string memberName)
+ {
+ return reference.Target?
+ .GetType()
+ .GetRuntimeProperty(memberName)?
+ .GetCustomAttributes()
+ .Any() ?? false;
+ }
+
+ ///
+ public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor inner)
+ {
+ return new Accessor(reference, name, inner);
+ }
+
+ private class Accessor : DataValidatiorBase
+ {
+ private ValidationContext _context;
+
+ public Accessor(WeakReference reference, string name, IPropertyAccessor inner)
+ : base(inner)
+ {
+ _context = new ValidationContext(reference.Target);
+ _context.MemberName = name;
+ }
+
+ public override bool SetValue(object value, BindingPriority priority)
+ {
+ return base.SetValue(value, priority);
+ }
+
+ protected override void InnerValueChanged(object value)
+ {
+ var errors = new List();
+
+ if (Validator.TryValidateProperty(value, _context, errors))
+ {
+ base.InnerValueChanged(value);
+ }
+ else
+ {
+ base.InnerValueChanged(new BindingNotification(
+ CreateException(errors),
+ BindingErrorType.DataValidationError,
+ value));
+ }
+ }
+
+ private Exception CreateException(IList errors)
+ {
+ if (errors.Count == 1)
+ {
+ return new ValidationException(errors[0].ErrorMessage);
+ }
+ else
+ {
+ return new AggregateException(
+ errors.Select(x => new ValidationException(x.ErrorMessage)));
+ }
+ }
+ }
+ }
+}
diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/DataValidatiorBase.cs b/src/Markup/Avalonia.Markup/Data/Plugins/DataValidatiorBase.cs
new file mode 100644
index 0000000000..95d269f437
--- /dev/null
+++ b/src/Markup/Avalonia.Markup/Data/Plugins/DataValidatiorBase.cs
@@ -0,0 +1,80 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using Avalonia.Data;
+
+namespace Avalonia.Markup.Data.Plugins
+{
+ ///
+ /// Base class for data validators.
+ ///
+ ///
+ /// Data validators are s that are returned from an
+ /// . They wrap an inner
+ /// and convert any values received from the inner property accessor into
+ /// s.
+ ///
+ public abstract class DataValidatiorBase : PropertyAccessorBase, IObserver
+ {
+ private readonly IPropertyAccessor _inner;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The inner property accessor.
+ protected DataValidatiorBase(IPropertyAccessor inner)
+ {
+ _inner = inner;
+ }
+
+ ///
+ public override Type PropertyType => _inner.PropertyType;
+
+ ///
+ public override object Value => _inner.Value;
+
+ ///
+ public override bool SetValue(object value, BindingPriority priority) => _inner.SetValue(value, priority);
+
+ ///
+ /// Should never be called: the inner should never notify
+ /// completion.
+ ///
+ void IObserver.OnCompleted() { }
+
+ ///
+ /// Should never be called: the inner should never notify
+ /// an error.
+ ///
+ void IObserver.OnError(Exception error) { }
+
+ ///
+ /// Called when the inner notifies with a new value.
+ ///
+ /// The value.
+ void IObserver.OnNext(object value) => InnerValueChanged(value);
+
+ ///
+ protected override void Dispose(bool disposing) => _inner.Dispose();
+
+ ///
+ /// Begins listening to the inner .
+ ///
+ protected override void SubscribeCore(IObserver observer) => _inner.Subscribe(this);
+
+ ///
+ /// Called when the inner notifies with a new value.
+ ///
+ /// The value.
+ ///
+ /// Notifies the observer that the value has changed. The value will be wrapped in a
+ /// if it is not already a binding notification.
+ ///
+ protected virtual void InnerValueChanged(object value)
+ {
+ var notification = value as BindingNotification ?? new BindingNotification(value);
+ Observer.OnNext(notification);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/ExceptionValidationPlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/ExceptionValidationPlugin.cs
index 95a32a6928..e0b6bcfd7c 100644
--- a/src/Markup/Avalonia.Markup/Data/Plugins/ExceptionValidationPlugin.cs
+++ b/src/Markup/Avalonia.Markup/Data/Plugins/ExceptionValidationPlugin.cs
@@ -10,23 +10,21 @@ namespace Avalonia.Markup.Data.Plugins
///
/// Validates properties that report errors by throwing exceptions.
///
- public class ExceptionValidationPlugin : IValidationPlugin
+ public class ExceptionValidationPlugin : IDataValidationPlugin
{
- public static ExceptionValidationPlugin Instance { get; } = new ExceptionValidationPlugin();
-
///
- public bool Match(WeakReference reference) => true;
+ public bool Match(WeakReference reference, string memberName) => true;
///
- public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor, Action callback)
+ public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor inner)
{
- return new ExceptionValidationChecker(reference, name, accessor, callback);
+ return new Validator(reference, name, inner);
}
- private class ExceptionValidationChecker : ValidatingPropertyAccessorBase
+ private class Validator : DataValidatiorBase
{
- public ExceptionValidationChecker(WeakReference reference, string name, IPropertyAccessor accessor, Action callback)
- : base(reference, name, accessor, callback)
+ public Validator(WeakReference reference, string name, IPropertyAccessor inner)
+ : base(inner)
{
}
@@ -34,39 +32,19 @@ namespace Avalonia.Markup.Data.Plugins
{
try
{
- var success = base.SetValue(value, priority);
- SendValidationCallback(new ExceptionValidationStatus(null));
- return success;
+ return base.SetValue(value, priority);
}
catch (TargetInvocationException ex)
{
- SendValidationCallback(new ExceptionValidationStatus(ex.InnerException));
+ Observer.OnNext(new BindingNotification(ex.InnerException, BindingErrorType.DataValidationError));
}
catch (Exception ex)
{
- SendValidationCallback(new ExceptionValidationStatus(ex));
+ Observer.OnNext(new BindingNotification(ex, BindingErrorType.DataValidationError));
}
- return false;
- }
- }
- ///
- /// Describes the current validation status after setting a property value.
- ///
- public class ExceptionValidationStatus : IValidationStatus
- {
- internal ExceptionValidationStatus(Exception exception)
- {
- Exception = exception;
+ return false;
}
-
- ///
- /// The thrown exception. If there was no thrown exception, null.
- ///
- public Exception Exception { get; }
-
- ///
- public bool IsValid => Exception == null;
}
}
}
diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/IDataValidationPlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/IDataValidationPlugin.cs
new file mode 100644
index 0000000000..0952e2edab
--- /dev/null
+++ b/src/Markup/Avalonia.Markup/Data/Plugins/IDataValidationPlugin.cs
@@ -0,0 +1,37 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using Avalonia.Data;
+
+namespace Avalonia.Markup.Data.Plugins
+{
+ ///
+ /// Defines how data validation is observed by an .
+ ///
+ public interface IDataValidationPlugin
+ {
+ ///
+ /// Checks whether this plugin can handle data validation on the specified object.
+ ///
+ /// A weak reference to the object.
+ /// The name of the member to validate.
+ /// True if the plugin can handle the object; otherwise false.
+ bool Match(WeakReference reference, string memberName);
+
+ ///
+ /// Starts monitoring the data validation state of a property on an object.
+ ///
+ /// A weak reference to the object.
+ /// The property name.
+ /// The inner property accessor used to aceess the property.
+ ///
+ /// An interface through which future interactions with the
+ /// property will be made.
+ ///
+ IPropertyAccessor Start(
+ WeakReference reference,
+ string propertyName,
+ IPropertyAccessor inner);
+ }
+}
diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessor.cs b/src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessor.cs
index d9da4b906f..9e686baf10 100644
--- a/src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessor.cs
+++ b/src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessor.cs
@@ -10,11 +10,14 @@ namespace Avalonia.Markup.Data.Plugins
/// Defines an accessor to a property on an object returned by a
///
///
- public interface IPropertyAccessor : IDisposable
+ public interface IPropertyAccessor : IObservable, IDisposable
{
///
/// Gets the type of the property.
///
+ ///
+ /// The accessor has not been subscribed to yet.
+ ///
Type PropertyType { get; }
///
diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessorPlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessorPlugin.cs
index 0d6b57e424..187142bd0e 100644
--- a/src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessorPlugin.cs
+++ b/src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessorPlugin.cs
@@ -2,7 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
-using System.Collections;
namespace Avalonia.Markup.Data.Plugins
{
@@ -24,14 +23,12 @@ namespace Avalonia.Markup.Data.Plugins
///
/// A weak reference to the object.
/// The property name.
- /// A function to call when the property changes.
///
/// An interface through which future interactions with the
/// property will be made.
///
IPropertyAccessor Start(
WeakReference reference,
- string propertyName,
- Action changed);
+ string propertyName);
}
}
diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/IValidationPlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/IValidationPlugin.cs
deleted file mode 100644
index e42e1ad7b2..0000000000
--- a/src/Markup/Avalonia.Markup/Data/Plugins/IValidationPlugin.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using Avalonia.Data;
-
-namespace Avalonia.Markup.Data.Plugins
-{
- ///
- /// Defines how view model data validation is observed by an .
- ///
- public interface IValidationPlugin
- {
-
- ///
- /// Checks whether the data uses a validation scheme supported by this plugin.
- ///
- /// A weak reference to the data.
- /// true if this plugin can observe the validation; otherwise, false.
- bool Match(WeakReference reference);
-
- ///
- /// Starts monitoring the validation state of an object for the given property.
- ///
- /// A weak reference to the object.
- /// The property name.
- /// An underlying to access the property.
- /// A function to call when the validation state changes.
- ///
- /// A subclass through which future interactions with the
- /// property will be made.
- ///
- IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor, Action callback);
- }
-}
diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/IValuePlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/IValuePlugin.cs
new file mode 100644
index 0000000000..fb285c6d73
--- /dev/null
+++ b/src/Markup/Avalonia.Markup/Data/Plugins/IValuePlugin.cs
@@ -0,0 +1,29 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+
+namespace Avalonia.Markup.Data.Plugins
+{
+ ///
+ /// Defines how values are observed by an .
+ ///
+ public interface IValuePlugin
+ {
+ ///
+ /// Checks whether this plugin handles the specified value.
+ ///
+ /// A weak reference to the value.
+ /// True if the plugin can handle the value; otherwise false.
+ bool Match(WeakReference reference);
+
+ ///
+ /// Starts producing output based on the specified value.
+ ///
+ /// A weak reference to the object.
+ ///
+ /// An observable that produces the output for the value.
+ ///
+ IObservable Start(WeakReference reference);
+ }
+}
diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/IndeiValidationPlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/IndeiValidationPlugin.cs
index f6b4aea614..82bc87c207 100644
--- a/src/Markup/Avalonia.Markup/Data/Plugins/IndeiValidationPlugin.cs
+++ b/src/Markup/Avalonia.Markup/Data/Plugins/IndeiValidationPlugin.cs
@@ -2,7 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
-using System.Collections;
+using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Avalonia.Data;
@@ -13,79 +13,106 @@ namespace Avalonia.Markup.Data.Plugins
///
/// Validates properties on objects that implement .
///
- public class IndeiValidationPlugin : IValidationPlugin
+ public class IndeiValidationPlugin : IDataValidationPlugin
{
///
- public bool Match(WeakReference reference)
- {
- return reference.Target is INotifyDataErrorInfo;
- }
+ public bool Match(WeakReference reference, string memberName) => reference.Target is INotifyDataErrorInfo;
///
- public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor, Action callback)
+ public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor)
{
- return new IndeiValidationChecker(reference, name, accessor, callback);
+ return new Validator(reference, name, accessor);
}
- private class IndeiValidationChecker : ValidatingPropertyAccessorBase, IWeakSubscriber
+ private class Validator : DataValidatiorBase, IWeakSubscriber
{
- public IndeiValidationChecker(WeakReference reference, string name, IPropertyAccessor accessor, Action callback)
- : base(reference, name, accessor, callback)
+ WeakReference _reference;
+ string _name;
+
+ public Validator(WeakReference reference, string name, IPropertyAccessor inner)
+ : base(inner)
+ {
+ _reference = reference;
+ _name = name;
+ }
+
+ void IWeakSubscriber.OnEvent(object sender, DataErrorsChangedEventArgs e)
{
- var target = reference.Target as INotifyDataErrorInfo;
+ if (e.PropertyName == _name || string.IsNullOrEmpty(e.PropertyName))
+ {
+ Observer.OnNext(CreateBindingNotification(Value));
+ }
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+
+ var target = _reference.Target as INotifyDataErrorInfo;
+
if (target != null)
{
- if (target.HasErrors)
- {
- SendValidationCallback(new IndeiValidationStatus(target.GetErrors(name)));
- }
- WeakSubscriptionManager.Subscribe(
+ WeakSubscriptionManager.Unsubscribe(
target,
nameof(target.ErrorsChanged),
this);
}
}
- public override void Dispose()
+ protected override void SubscribeCore(IObserver observer)
{
- base.Dispose();
var target = _reference.Target as INotifyDataErrorInfo;
+
if (target != null)
{
- WeakSubscriptionManager.Unsubscribe(
+ WeakSubscriptionManager.Subscribe(
target,
nameof(target.ErrorsChanged),
this);
}
+
+ base.SubscribeCore(observer);
}
- public void OnEvent(object sender, DataErrorsChangedEventArgs e)
+ protected override void InnerValueChanged(object value)
{
- if (e.PropertyName == _name || string.IsNullOrEmpty(e.PropertyName))
+ base.InnerValueChanged(CreateBindingNotification(value));
+ }
+
+ private BindingNotification CreateBindingNotification(object value)
+ {
+ var target = (INotifyDataErrorInfo)_reference.Target;
+
+ if (target != null)
{
- var indei = _reference.Target as INotifyDataErrorInfo;
- SendValidationCallback(new IndeiValidationStatus(indei.GetErrors(e.PropertyName)));
+ var errors = target.GetErrors(_name)?
+ .Cast()
+ .Where(x => x != null).ToList();
+
+ if (errors?.Count > 0)
+ {
+ return new BindingNotification(
+ GenerateException(errors),
+ BindingErrorType.DataValidationError,
+ value);
+ }
}
+
+ return new BindingNotification(value);
}
- }
- ///
- /// Describes the current validation status of a property as reported by an object that implements .
- ///
- public class IndeiValidationStatus : IValidationStatus
- {
- internal IndeiValidationStatus(IEnumerable errors)
+ private Exception GenerateException(IList errors)
{
- Errors = errors;
+ if (errors.Count == 1)
+ {
+ return new Exception(errors[0]);
+ }
+ else
+ {
+ return new AggregateException(
+ errors.Select(x => new Exception(x)));
+ }
}
-
- ///
- public bool IsValid => !Errors?.OfType().Any() ?? true;
-
- ///
- /// The errors on the given property and on the object as a whole.
- ///
- public IEnumerable Errors { get; }
}
}
}
diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs
index 36046dd742..138f09b373 100644
--- a/src/Markup/Avalonia.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs
+++ b/src/Markup/Avalonia.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs
@@ -9,7 +9,6 @@ using System.Reflection;
using Avalonia.Data;
using Avalonia.Logging;
using Avalonia.Utilities;
-using System.Collections;
namespace Avalonia.Markup.Data.Plugins
{
@@ -19,124 +18,130 @@ namespace Avalonia.Markup.Data.Plugins
///
public class InpcPropertyAccessorPlugin : IPropertyAccessorPlugin
{
- ///
- /// Checks whether this plugin can handle accessing the properties of the specified object.
- ///
- /// The object.
- /// True if the plugin can handle the object; otherwise false.
- public bool Match(WeakReference reference)
- {
- Contract.Requires(reference != null);
-
- return true;
- }
+ ///
+ public bool Match(WeakReference reference) => true;
///
/// Starts monitoring the value of a property on an object.
///
/// The object.
/// The property name.
- /// A function to call when the property changes.
///
/// An interface through which future interactions with the
/// property will be made.
///
- public IPropertyAccessor Start(
- WeakReference reference,
- string propertyName,
- Action changed)
+ public IPropertyAccessor Start(WeakReference reference, string propertyName)
{
Contract.Requires(reference != null);
Contract.Requires(propertyName != null);
- Contract.Requires(changed != null);
var instance = reference.Target;
var p = instance.GetType().GetRuntimeProperties().FirstOrDefault(_ => _.Name == propertyName);
if (p != null)
{
- return new Accessor(reference, p, changed);
+ return new Accessor(reference, p);
}
else
{
var message = $"Could not find CLR property '{propertyName}' on '{instance}'";
var exception = new MissingMemberException(message);
- return new PropertyError(new BindingError(exception));
+ return new PropertyError(new BindingNotification(exception, BindingErrorType.Error));
}
}
- private class Accessor : IPropertyAccessor, IWeakSubscriber
+ private class Accessor : PropertyAccessorBase, IWeakSubscriber
{
private readonly WeakReference _reference;
private readonly PropertyInfo _property;
- private readonly Action _changed;
+ private bool _eventRaised;
- public Accessor(
- WeakReference reference,
- PropertyInfo property,
- Action changed)
+ public Accessor(WeakReference reference, PropertyInfo property)
{
Contract.Requires(reference != null);
Contract.Requires(property != null);
_reference = reference;
_property = property;
- _changed = changed;
+ }
- var inpc = reference.Target as INotifyPropertyChanged;
+ public override Type PropertyType => _property.PropertyType;
- if (inpc != null)
+ public override object Value
+ {
+ get
{
- WeakSubscriptionManager.Subscribe(
- inpc,
- nameof(inpc.PropertyChanged),
- this);
+ var o = _reference.Target;
+ return (o != null) ? _property.GetValue(o) : null;
}
- else
+ }
+
+ public override bool SetValue(object value, BindingPriority priority)
+ {
+ if (_property.CanWrite)
{
- Logger.Warning(
- LogArea.Binding,
- this,
- "Bound to property {Property} on {Source} which does not implement INotifyPropertyChanged",
- property.Name,
- reference.Target,
- reference.Target.GetType());
+ _eventRaised = false;
+ _property.SetValue(_reference.Target, value);
+
+ if (!_eventRaised)
+ {
+ SendCurrentValue();
+ }
+
+ return true;
}
- }
- public Type PropertyType => _property.PropertyType;
+ return false;
+ }
- public object Value => _property.GetValue(_reference.Target);
+ void IWeakSubscriber.OnEvent(object sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName))
+ {
+ _eventRaised = true;
+ SendCurrentValue();
+ }
+ }
- public void Dispose()
+ protected override void Dispose(bool disposing)
{
var inpc = _reference.Target as INotifyPropertyChanged;
if (inpc != null)
{
- WeakSubscriptionManager.Unsubscribe(
+ WeakSubscriptionManager.Unsubscribe(
inpc,
nameof(inpc.PropertyChanged),
this);
}
}
- public bool SetValue(object value, BindingPriority priority)
+ protected override void SubscribeCore(IObserver observer)
{
- if (_property.CanWrite)
+ SendCurrentValue();
+ SubscribeToChanges();
+ }
+
+ private void SendCurrentValue()
+ {
+ try
{
- _property.SetValue(_reference.Target, value);
- return true;
+ var value = Value;
+ Observer.OnNext(value);
}
-
- return false;
+ catch { }
}
- void IWeakSubscriber.OnEvent(object sender, PropertyChangedEventArgs e)
+ private void SubscribeToChanges()
{
- if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName))
+ var inpc = _reference.Target as INotifyPropertyChanged;
+
+ if (inpc != null)
{
- _changed(Value);
+ WeakSubscriptionManager.Subscribe(
+ inpc,
+ nameof(inpc.PropertyChanged),
+ this);
}
}
}
diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/ObservableValuePlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/ObservableValuePlugin.cs
new file mode 100644
index 0000000000..a406fc55b9
--- /dev/null
+++ b/src/Markup/Avalonia.Markup/Data/Plugins/ObservableValuePlugin.cs
@@ -0,0 +1,44 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+using System.Reflection;
+using System.Threading.Tasks;
+using System.Windows.Input;
+using Avalonia.Data;
+
+namespace Avalonia.Markup.Data.Plugins
+{
+ ///
+ /// Handles binding to s in an .
+ ///
+ public class ObservableValuePlugin : IValuePlugin
+ {
+ ///
+ /// Checks whether this plugin handles the specified value.
+ ///
+ /// A weak reference to the value.
+ /// True if the plugin can handle the value; otherwise false.
+ public virtual bool Match(WeakReference reference)
+ {
+ var target = reference.Target;
+
+ // ReactiveCommand is an IObservable but we want to bind to it, not its value.
+ return target is IObservable && !(target is ICommand);
+ }
+
+ ///
+ /// Starts producing output based on the specified value.
+ ///
+ /// A weak reference to the object.
+ ///
+ /// An observable that produces the output for the value.
+ ///
+ public virtual IObservable Start(WeakReference reference)
+ {
+ return reference.Target as IObservable;
+ }
+ }
+}
diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/PropertyAccessorBase.cs b/src/Markup/Avalonia.Markup/Data/Plugins/PropertyAccessorBase.cs
new file mode 100644
index 0000000000..9aa858e0eb
--- /dev/null
+++ b/src/Markup/Avalonia.Markup/Data/Plugins/PropertyAccessorBase.cs
@@ -0,0 +1,68 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using Avalonia.Data;
+
+namespace Avalonia.Markup.Data.Plugins
+{
+ ///
+ /// Defines a default base implementation for a .
+ ///
+ ///
+ /// is an observable that will only be subscribed to one time.
+ /// In addition, the subscription can be disposed by calling on the
+ /// property accessor itself - this prevents needing to hold two references for a subscription.
+ ///
+ public abstract class PropertyAccessorBase : IPropertyAccessor
+ {
+ ///
+ public abstract Type PropertyType { get; }
+
+ ///
+ public abstract object Value { get; }
+
+ ///
+ /// Stops the subscription.
+ ///
+ public void Dispose() => Dispose(true);
+
+ ///
+ public abstract bool SetValue(object value, BindingPriority priority);
+
+ ///
+ /// The currently subscribed observer.
+ ///
+ protected IObserver Observer { get; private set; }
+
+ ///
+ public IDisposable Subscribe(IObserver observer)
+ {
+ Contract.Requires(observer != null);
+
+ if (Observer != null)
+ {
+ throw new InvalidOperationException(
+ "A property accessor can be subscribed to only once.");
+ }
+
+ Observer = observer;
+ SubscribeCore(observer);
+ return this;
+ }
+
+ ///
+ /// Stops listening to the property.
+ ///
+ ///
+ /// True if the method was called, false if the object is being
+ /// finalized.
+ ///
+ protected virtual void Dispose(bool disposing) => Observer = null;
+
+ ///
+ /// When overridden in a derived class, begins listening to the property.
+ ///
+ protected abstract void SubscribeCore(IObserver observer);
+ }
+}
diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/PropertyError.cs b/src/Markup/Avalonia.Markup/Data/Plugins/PropertyError.cs
index f73bb1fc94..b351ef39bd 100644
--- a/src/Markup/Avalonia.Markup/Data/Plugins/PropertyError.cs
+++ b/src/Markup/Avalonia.Markup/Data/Plugins/PropertyError.cs
@@ -1,4 +1,5 @@
using System;
+using System.Reactive.Disposables;
using Avalonia.Data;
namespace Avalonia.Markup.Data.Plugins
@@ -8,13 +9,13 @@ namespace Avalonia.Markup.Data.Plugins
///
public class PropertyError : IPropertyAccessor
{
- private BindingError _error;
+ private BindingNotification _error;
///
/// Initializes a new instance of the class.
///
/// The error to report.
- public PropertyError(BindingError error)
+ public PropertyError(BindingNotification error)
{
_error = error;
}
@@ -35,5 +36,11 @@ namespace Avalonia.Markup.Data.Plugins
{
return false;
}
+
+ public IDisposable Subscribe(IObserver observer)
+ {
+ observer.OnNext(_error);
+ return Disposable.Empty;
+ }
}
}
diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/TaskValuePlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/TaskValuePlugin.cs
new file mode 100644
index 0000000000..b6fda67503
--- /dev/null
+++ b/src/Markup/Avalonia.Markup/Data/Plugins/TaskValuePlugin.cs
@@ -0,0 +1,82 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Reactive.Concurrency;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+using System.Reflection;
+using System.Threading.Tasks;
+using Avalonia.Data;
+
+namespace Avalonia.Markup.Data.Plugins
+{
+ ///
+ /// Handles binding to s in an .
+ ///
+ public class TaskValuePlugin : IValuePlugin
+ {
+ ///
+ /// Checks whether this plugin handles the specified value.
+ ///
+ /// A weak reference to the value.
+ /// True if the plugin can handle the value; otherwise false.
+ public virtual bool Match(WeakReference reference) => reference.Target is Task;
+
+ ///
+ /// Starts producing output based on the specified value.
+ ///
+ /// A weak reference to the object.
+ ///
+ /// An observable that produces the output for the value.
+ ///
+ public virtual IObservable Start(WeakReference reference)
+ {
+ var task = reference.Target as Task;
+
+ if (task != null)
+ {
+ var resultProperty = task.GetType().GetTypeInfo().GetDeclaredProperty("Result");
+
+ if (resultProperty != null)
+ {
+ switch (task.Status)
+ {
+ case TaskStatus.RanToCompletion:
+ case TaskStatus.Faulted:
+ return HandleCompleted(task);
+ default:
+ var subject = new Subject();
+ task.ContinueWith(
+ x => HandleCompleted(task).Subscribe(subject),
+ TaskScheduler.FromCurrentSynchronizationContext())
+ .ConfigureAwait(false);
+ return subject;
+ }
+ }
+ }
+
+ return Observable.Empty();
+ }
+
+ protected IObservable HandleCompleted(Task task)
+ {
+ var resultProperty = task.GetType().GetTypeInfo().GetDeclaredProperty("Result");
+
+ if (resultProperty != null)
+ {
+ switch (task.Status)
+ {
+ case TaskStatus.RanToCompletion:
+ return Observable.Return(resultProperty.GetValue(task));
+ case TaskStatus.Faulted:
+ return Observable.Return(new BindingNotification(task.Exception, BindingErrorType.Error));
+ default:
+ throw new AvaloniaInternalException("HandleCompleted called for non-completed Task.");
+ }
+ }
+
+ return Observable.Empty();
+ }
+ }
+}
diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/ValidatingPropertyAccessorBase.cs b/src/Markup/Avalonia.Markup/Data/Plugins/ValidatingPropertyAccessorBase.cs
deleted file mode 100644
index 07ac600a8a..0000000000
--- a/src/Markup/Avalonia.Markup/Data/Plugins/ValidatingPropertyAccessorBase.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-using System;
-using Avalonia.Data;
-
-namespace Avalonia.Markup.Data.Plugins
-{
-
- ///
- /// A base class for validating s that wraps an and forwards method calls to it.
- ///
- public abstract class ValidatingPropertyAccessorBase : IPropertyAccessor
- {
- protected readonly WeakReference _reference;
- protected readonly string _name;
- private readonly IPropertyAccessor _accessor;
- private readonly Action _callback;
-
- protected ValidatingPropertyAccessorBase(WeakReference reference, string name, IPropertyAccessor accessor, Action callback)
- {
- _reference = reference;
- _name = name;
- _accessor = accessor;
- _callback = callback;
- }
-
- ///
- public Type PropertyType => _accessor.PropertyType;
-
- ///
- public object Value => _accessor.Value;
-
- ///
- public virtual void Dispose() => _accessor.Dispose();
-
- ///
- public virtual bool SetValue(object value, BindingPriority priority) => _accessor.SetValue(value, priority);
-
- ///
- /// Sends the validation status to the callback specified in construction.
- ///
- /// The validation status.
- protected void SendValidationCallback(IValidationStatus status)
- {
- _callback?.Invoke(status);
- }
- }
-}
\ No newline at end of file
diff --git a/src/Markup/Avalonia.Markup/Data/PropertyAccessorNode.cs b/src/Markup/Avalonia.Markup/Data/PropertyAccessorNode.cs
index b12fbe90eb..7fb3137417 100644
--- a/src/Markup/Avalonia.Markup/Data/PropertyAccessorNode.cs
+++ b/src/Markup/Avalonia.Markup/Data/PropertyAccessorNode.cs
@@ -3,11 +3,8 @@
using System;
using System.Linq;
+using System.Reactive.Disposables;
using System.Reactive.Linq;
-using System.Reflection;
-using System.Threading;
-using System.Threading.Tasks;
-using System.Windows.Input;
using Avalonia.Data;
using Avalonia.Markup.Data.Plugins;
@@ -15,9 +12,8 @@ namespace Avalonia.Markup.Data
{
internal class PropertyAccessorNode : ExpressionNode
{
+ private readonly bool _enableValidation;
private IPropertyAccessor _accessor;
- private IDisposable _subscription;
- private bool _enableValidation;
public PropertyAccessorNode(string propertyName, bool enableValidation)
{
@@ -25,118 +21,44 @@ namespace Avalonia.Markup.Data
_enableValidation = enableValidation;
}
+ public override string Description => PropertyName;
public string PropertyName { get; }
-
public Type PropertyType => _accessor?.PropertyType;
- public override bool SetValue(object value, BindingPriority priority)
+ public bool SetTargetValue(object value, BindingPriority priority)
{
- if (Next != null)
+ if (_accessor != null)
{
- return Next.SetValue(value, priority);
+ try { return _accessor.SetValue(value, priority); } catch { }
}
- else
- {
- if (_accessor != null)
- {
- return _accessor.SetValue(value, priority);
- }
- return false;
- }
+ return false;
}
- protected override void SubscribeAndUpdate(WeakReference reference)
+ protected override IObservable StartListeningCore(WeakReference reference)
{
- var instance = reference.Target;
+ var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference));
+ var accessor = plugin?.Start(reference, PropertyName);
- if (instance != null && instance != AvaloniaProperty.UnsetValue)
+ if (_enableValidation && Next == null)
{
- var accessorPlugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference));
-
- if (accessorPlugin != null)
+ foreach (var validator in ExpressionObserver.DataValidators)
{
- _accessor = ExceptionValidationPlugin.Instance.Start(
- reference,
- PropertyName,
- accessorPlugin.Start(reference, PropertyName, SetCurrentValue),
- SendValidationStatus);
-
- if (_enableValidation)
- {
- foreach (var validationPlugin in ExpressionObserver.ValidationCheckers)
- {
- if (validationPlugin.Match(reference))
- {
- _accessor = validationPlugin.Start(reference, PropertyName, _accessor, SendValidationStatus);
- }
- }
- }
-
- if (_accessor != null)
+ if (validator.Match(reference, PropertyName))
{
- SetCurrentValue(_accessor.Value);
- return;
+ accessor = validator.Start(reference, PropertyName, accessor);
}
}
}
- CurrentValue = UnsetReference;
- }
-
- protected override void Unsubscribe(object target)
- {
- _accessor?.Dispose();
- _accessor = null;
- }
-
- private void SetCurrentValue(object value)
- {
- var observable = value as IObservable;
- var command = value as ICommand;
- var task = value as Task;
- bool set = false;
-
- // HACK: ReactiveCommand is an IObservable but we want to bind to it, not its value.
- // We may need to make this a more general solution.
- if (observable != null && command == null)
- {
- CurrentValue = UnsetReference;
- set = true;
- _subscription = observable
- .ObserveOn(SynchronizationContext.Current)
- .Subscribe(x => CurrentValue = new WeakReference(x));
- }
- else if (task != null)
- {
- var resultProperty = task.GetType().GetTypeInfo().GetDeclaredProperty("Result");
-
- if (resultProperty != null)
+ // Ensure that _accessor is set for the duration of the subscription.
+ return Observable.Using(
+ () =>
{
- if (task.Status == TaskStatus.RanToCompletion)
- {
- CurrentValue = new WeakReference(resultProperty.GetValue(task));
- set = true;
- }
- else
- {
- task.ContinueWith(
- x => CurrentValue = new WeakReference(resultProperty.GetValue(task)),
- TaskScheduler.FromCurrentSynchronizationContext())
- .ConfigureAwait(false);
- }
- }
- }
- else
- {
- CurrentValue = new WeakReference(value);
- set = true;
- }
-
- if (!set)
- {
- CurrentValue = UnsetReference;
- }
+ _accessor = accessor;
+ return Disposable.Create(() => _accessor = null);
+ },
+ _ => accessor);
}
}
}
diff --git a/src/Markup/Avalonia.Markup/DefaultValueConverter.cs b/src/Markup/Avalonia.Markup/DefaultValueConverter.cs
index 469b00d3ad..86d37d8e13 100644
--- a/src/Markup/Avalonia.Markup/DefaultValueConverter.cs
+++ b/src/Markup/Avalonia.Markup/DefaultValueConverter.cs
@@ -43,8 +43,18 @@ namespace Avalonia.Markup
if (value != null)
{
- var message = $"Could not convert '{value}' to '{targetType}'";
- return new BindingError(new InvalidCastException(message));
+ string message;
+
+ if (TypeUtilities.IsNumeric(targetType))
+ {
+ message = $"'{value}' is not a valid number.";
+ }
+ else
+ {
+ message = $"Could not convert '{value}' to '{targetType.Name}'.";
+ }
+
+ return new BindingNotification(new InvalidCastException(message), BindingErrorType.Error);
}
return AvaloniaProperty.UnsetValue;
diff --git a/src/Markup/Avalonia.Markup/IValueConverter.cs b/src/Markup/Avalonia.Markup/IValueConverter.cs
index 23117a3fac..10d5c626c2 100644
--- a/src/Markup/Avalonia.Markup/IValueConverter.cs
+++ b/src/Markup/Avalonia.Markup/IValueConverter.cs
@@ -3,6 +3,7 @@
using System;
using System.Globalization;
+using Avalonia.Data;
namespace Avalonia.Markup
{
@@ -21,8 +22,8 @@ namespace Avalonia.Markup
/// The converted value.
///
/// This method should not throw exceptions. If the value is not convertible, return
- /// . Any exception thrown will be treated as
- /// an application exception.
+ /// a in an error state. Any exceptions thrown will be
+ /// treated as an application exception.
///
object Convert(object value, Type targetType, object parameter, CultureInfo culture);
@@ -36,8 +37,8 @@ namespace Avalonia.Markup
/// The converted value.
///
/// This method should not throw exceptions. If the value is not convertible, return
- /// . Any exception thrown will be treated as
- /// an application exception.
+ /// a in an error state. Any exceptions thrown will be
+ /// treated as an application exception.
///
object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture);
}
diff --git a/src/Markup/Avalonia.Markup/Properties/AssemblyInfo.cs b/src/Markup/Avalonia.Markup/Properties/AssemblyInfo.cs
index d74ccf00a1..dd8c0a6bd3 100644
--- a/src/Markup/Avalonia.Markup/Properties/AssemblyInfo.cs
+++ b/src/Markup/Avalonia.Markup/Properties/AssemblyInfo.cs
@@ -8,4 +8,3 @@ using System.Runtime.CompilerServices;
[assembly: AssemblyTitle("Avalonia.Markup")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Markup")]
[assembly: InternalsVisibleTo("Avalonia.Markup.UnitTests")]
-[assembly: InternalsVisibleTo("Avalonia.Markup.Xaml")]
\ No newline at end of file
diff --git a/src/Markup/Avalonia.Markup/packages.config b/src/Markup/Avalonia.Markup/packages.config
index 9f732f1bcb..bcef21429a 100644
--- a/src/Markup/Avalonia.Markup/packages.config
+++ b/src/Markup/Avalonia.Markup/packages.config
@@ -1,5 +1,6 @@
+
diff --git a/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryContextImpl.cs
index 5b8e8724d8..4228745d6b 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryContextImpl.cs
+++ b/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryContextImpl.cs
@@ -6,6 +6,8 @@ using Avalonia.Platform;
using SharpDX.Direct2D1;
using SweepDirection = SharpDX.Direct2D1.SweepDirection;
using D2D = SharpDX.Direct2D1;
+using Avalonia.Logging;
+using System;
namespace Avalonia.Direct2D1.Media
{
@@ -76,7 +78,20 @@ namespace Avalonia.Direct2D1.Media
public void Dispose()
{
- _sink.Close();
+ // Put a catch around sink.Close as it may throw if there were an error e.g. parsing a path.
+ try
+ {
+ _sink.Close();
+ }
+ catch (Exception ex)
+ {
+ Logger.Error(
+ LogArea.Visual,
+ this,
+ "GeometrySink.Close exception: {Exception}",
+ ex);
+ }
+
_sink.Dispose();
}
}
diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj
index 3fdde9339f..074fe84914 100644
--- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj
+++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj
@@ -38,6 +38,8 @@
CS1591
+
+
@@ -53,12 +55,19 @@
+
+
+
Properties\SharedAssemblyInfo.cs
+
+ Component
+
+
diff --git a/src/Windows/Avalonia.Win32/Embedding/EmbeddedWindowImpl.cs b/src/Windows/Avalonia.Win32/Embedding/EmbeddedWindowImpl.cs
index baa4cd4460..d27c28cb67 100644
--- a/src/Windows/Avalonia.Win32/Embedding/EmbeddedWindowImpl.cs
+++ b/src/Windows/Avalonia.Win32/Embedding/EmbeddedWindowImpl.cs
@@ -2,14 +2,17 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
+using Avalonia.Platform;
using Avalonia.Win32.Interop;
namespace Avalonia.Win32
{
- public class EmbeddedWindowImpl : WindowImpl
+ class EmbeddedWindowImpl : WindowImpl, IEmbeddableWindowImpl
{
private static readonly System.Windows.Forms.UserControl WinFormsControl = new System.Windows.Forms.UserControl();
+ public static IntPtr DefaultParentWindow = WinFormsControl.Handle;
+
protected override IntPtr CreateWindowOverride(ushort atom)
{
var hWnd = UnmanagedMethods.CreateWindowEx(
@@ -21,11 +24,20 @@ namespace Avalonia.Win32
0,
640,
480,
- WinFormsControl.Handle,
+ DefaultParentWindow,
IntPtr.Zero,
IntPtr.Zero,
IntPtr.Zero);
return hWnd;
}
+
+ protected override IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
+ {
+ if(msg == (uint)UnmanagedMethods.WindowsMessage.WM_KILLFOCUS)
+ LostFocus?.Invoke();
+ return base.WndProc(hWnd, msg, wParam, lParam);
+ }
+
+ public event Action LostFocus;
}
}
diff --git a/src/Windows/Avalonia.Win32/Embedding/WinFormsAvaloniaControlHost.cs b/src/Windows/Avalonia.Win32/Embedding/WinFormsAvaloniaControlHost.cs
new file mode 100644
index 0000000000..3b52090493
--- /dev/null
+++ b/src/Windows/Avalonia.Win32/Embedding/WinFormsAvaloniaControlHost.cs
@@ -0,0 +1,91 @@
+using System;
+using System.ComponentModel;
+using System.Windows.Forms;
+using Avalonia.Controls;
+using Avalonia.Controls.Embedding;
+using Avalonia.Input;
+using Avalonia.VisualTree;
+using Avalonia.Win32.Interop;
+using WinFormsControl = System.Windows.Forms.Control;
+
+namespace Avalonia.Win32.Embedding
+{
+ [ToolboxItem(true)]
+ public class WinFormsAvaloniaControlHost : WinFormsControl
+ {
+ private readonly EmbeddableControlRoot _root = new EmbeddableControlRoot();
+
+ public WinFormsAvaloniaControlHost()
+ {
+ SetStyle(ControlStyles.AllPaintingInWmPaint, true);
+ UnmanagedMethods.SetParent(_root.PlatformImpl.Handle.Handle, Handle);
+ _root.Prepare();
+ if (_root.IsFocused)
+ FocusManager.Instance.Focus(null);
+ _root.GotFocus += RootGotFocus;
+ _root.PlatformImpl.LostFocus += PlatformImpl_LostFocus;
+ FixPosition();
+ }
+
+ public Avalonia.Controls.Control Content
+ {
+ get { return (Avalonia.Controls.Control)_root.Content; }
+ set { _root.Content = value; }
+ }
+
+ void Unfocus()
+ {
+ var focused = (IVisual)FocusManager.Instance.Current;
+ if (focused == null)
+ return;
+ while (focused.VisualParent != null)
+ focused = focused.VisualParent;
+
+ if (focused == _root)
+ KeyboardDevice.Instance.SetFocusedElement(null, NavigationMethod.Unspecified, InputModifiers.None);
+ }
+
+ private void PlatformImpl_LostFocus()
+ {
+ Unfocus();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ _root.Dispose();
+ base.Dispose(disposing);
+ }
+
+ private void RootGotFocus(object sender, Interactivity.RoutedEventArgs e)
+ {
+ UnmanagedMethods.SetFocus(_root.PlatformImpl.Handle.Handle);
+ }
+
+ protected override void OnGotFocus(EventArgs e)
+ {
+ if (_root != null)
+ UnmanagedMethods.SetFocus(_root.PlatformImpl.Handle.Handle);
+ }
+
+
+ void FixPosition()
+ {
+ if (_root != null && Width > 0 && Height > 0)
+ UnmanagedMethods.MoveWindow(_root.PlatformImpl.Handle.Handle, 0, 0, Width, Height, true);
+ }
+
+
+
+ protected override void OnResize(EventArgs e)
+ {
+ FixPosition();
+ base.OnResize(e);
+ }
+
+ protected override void OnPaint(PaintEventArgs e)
+ {
+
+ }
+ }
+}
diff --git a/src/Windows/Avalonia.Win32/Embedding/WpfAvaloniaControlHost.cs b/src/Windows/Avalonia.Win32/Embedding/WpfAvaloniaControlHost.cs
new file mode 100644
index 0000000000..663f6906ed
--- /dev/null
+++ b/src/Windows/Avalonia.Win32/Embedding/WpfAvaloniaControlHost.cs
@@ -0,0 +1,52 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Forms.Integration;
+using System.Windows.Interop;
+using Avalonia.Controls;
+using Avalonia.Win32.Interop;
+
+namespace Avalonia.Win32.Embedding
+{
+ public class WpfAvaloniaControlHost : HwndHost
+ {
+ private WinFormsAvaloniaControlHost _host;
+ private Avalonia.Controls.Control _content;
+
+ public Avalonia.Controls.Control Content
+ {
+ get { return _content; }
+ set
+ {
+ if (_host != null)
+ _host.Content = value;
+ _content = value;
+
+ }
+ }
+
+ void DestroyHost()
+ {
+ _host?.Dispose();
+ _host = null;
+ }
+
+ protected override HandleRef BuildWindowCore(HandleRef hwndParent)
+ {
+ DestroyHost();
+ _host = new WinFormsAvaloniaControlHost {Content = _content};
+ UnmanagedMethods.SetParent(_host.Handle, hwndParent.Handle);
+ return new HandleRef(this, _host.Handle);
+ }
+
+ protected override void DestroyWindowCore(HandleRef hwnd)
+ {
+ DestroyHost();
+ }
+ }
+}
diff --git a/src/Windows/Avalonia.Win32/Input/KeyInterop.cs b/src/Windows/Avalonia.Win32/Input/KeyInterop.cs
index 7f05a65095..11c0a6dca9 100644
--- a/src/Windows/Avalonia.Win32/Input/KeyInterop.cs
+++ b/src/Windows/Avalonia.Win32/Input/KeyInterop.cs
@@ -8,7 +8,7 @@ using Avalonia.Win32.Interop;
namespace Avalonia.Win32.Input
{
- public static class KeyInterop
+ static class KeyInterop
{
private static readonly Dictionary s_virtualKeyFromKey = new Dictionary
{
diff --git a/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs b/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs
index e241420d7d..fee1fe2ae6 100644
--- a/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs
+++ b/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs
@@ -8,7 +8,7 @@ using Avalonia.Win32.Interop;
namespace Avalonia.Win32.Input
{
- public class WindowsKeyboardDevice : KeyboardDevice
+ class WindowsKeyboardDevice : KeyboardDevice
{
private readonly byte[] _keyStates = new byte[256];
diff --git a/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs b/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs
index a521c5fc8c..a55c808415 100644
--- a/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs
+++ b/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs
@@ -8,7 +8,7 @@ using Avalonia.Win32.Interop;
namespace Avalonia.Win32.Input
{
- public class WindowsMouseDevice : MouseDevice
+ class WindowsMouseDevice : MouseDevice
{
public new static WindowsMouseDevice Instance { get; } = new WindowsMouseDevice();
diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
index 8734bc9cac..ae030df49a 100644
--- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
+++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
@@ -727,10 +727,13 @@ namespace Avalonia.Win32.Interop
[DllImport("user32.dll")]
public static extern IntPtr SetTimer(IntPtr hWnd, IntPtr nIDEvent, uint uElapse, TimerProc lpTimerFunc);
-
+ [DllImport("user32.dll", SetLastError = true)]
+ public static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint);
[DllImport("user32.dll")]
public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, SetWindowPosFlags uFlags);
[DllImport("user32.dll")]
+ public static extern bool SetFocus(IntPtr hWnd);
+ [DllImport("user32.dll")]
public static extern bool SetParent(IntPtr hWnd, IntPtr hWndNewParent);
[DllImport("user32.dll")]
public static extern bool ShowWindow(IntPtr hWnd, ShowWindowCommand nCmdShow);
diff --git a/src/Windows/Avalonia.Win32/PlatformConstants.cs b/src/Windows/Avalonia.Win32/PlatformConstants.cs
index 34114174e0..ba89c5bee6 100644
--- a/src/Windows/Avalonia.Win32/PlatformConstants.cs
+++ b/src/Windows/Avalonia.Win32/PlatformConstants.cs
@@ -3,7 +3,7 @@
namespace Avalonia.Win32
{
- public static class PlatformConstants
+ static class PlatformConstants
{
public const string WindowHandleType = "HWND";
public const string CursorHandleType = "HCURSOR";
diff --git a/src/Windows/Avalonia.Win32/PopupImpl.cs b/src/Windows/Avalonia.Win32/PopupImpl.cs
index ca2ba201d3..7849bd6a9d 100644
--- a/src/Windows/Avalonia.Win32/PopupImpl.cs
+++ b/src/Windows/Avalonia.Win32/PopupImpl.cs
@@ -7,7 +7,7 @@ using Avalonia.Win32.Interop;
namespace Avalonia.Win32
{
- public class PopupImpl : WindowImpl, IPopupImpl
+ class PopupImpl : WindowImpl, IPopupImpl
{
public override void Show()
{
diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs
index ac2e62d27f..a7597ffb86 100644
--- a/src/Windows/Avalonia.Win32/Win32Platform.cs
+++ b/src/Windows/Avalonia.Win32/Win32Platform.cs
@@ -31,7 +31,7 @@ namespace Avalonia
namespace Avalonia.Win32
{
- public class Win32Platform : IPlatformThreadingInterface, IPlatformSettings, IWindowingPlatform, IPlatformIconLoader
+ class Win32Platform : IPlatformThreadingInterface, IPlatformSettings, IWindowingPlatform, IPlatformIconLoader
{
private static readonly Win32Platform s_instance = new Win32Platform();
private static Thread _uiThread;
@@ -177,7 +177,7 @@ namespace Avalonia.Win32
return new WindowImpl();
}
- public IWindowImpl CreateEmbeddableWindow()
+ public IEmbeddableWindowImpl CreateEmbeddableWindow()
{
return new EmbeddedWindowImpl();
}
diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs
index 0b09d9ec91..af68257357 100644
--- a/src/Windows/Avalonia.Win32/WindowImpl.cs
+++ b/src/Windows/Avalonia.Win32/WindowImpl.cs
@@ -18,7 +18,7 @@ using Avalonia.Win32.Interop;
namespace Avalonia.Win32
{
- public class WindowImpl : IWindowImpl
+ class WindowImpl : IWindowImpl
{
private static readonly List s_instances = new List();
diff --git a/src/iOS/Avalonia.iOS/WindowingPlatformImpl.cs b/src/iOS/Avalonia.iOS/WindowingPlatformImpl.cs
index 72adb5c086..b6a683e10a 100644
--- a/src/iOS/Avalonia.iOS/WindowingPlatformImpl.cs
+++ b/src/iOS/Avalonia.iOS/WindowingPlatformImpl.cs
@@ -20,7 +20,7 @@ namespace Avalonia.iOS
return _window;
}
- public IWindowImpl CreateEmbeddableWindow()
+ public IEmbeddableWindowImpl CreateEmbeddableWindow()
{
throw new NotImplementedException();
}
diff --git a/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj b/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj
index 6e4278fdfc..07ed7f14ca 100644
--- a/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj
+++ b/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj
@@ -90,6 +90,7 @@
+
diff --git a/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.v2.ncrunchproject b/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.v2.ncrunchproject
index 30815b1937..b5cd70b13f 100644
--- a/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.v2.ncrunchproject
+++ b/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.v2.ncrunchproject
@@ -17,10 +17,11 @@
true
true
60000
-
-
-
+
+
+
AutoDetect
STA
x86
+ LongTestTimesWithoutParallelExecution
\ No newline at end of file
diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs
index 2e7db2da05..66fe3c7767 100644
--- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs
+++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs
@@ -54,6 +54,36 @@ namespace Avalonia.Base.UnitTests
Assert.False(target.IsSet(Class1.QuxProperty));
}
+ [Fact]
+ public void OneTime_Binding_Ignores_UnsetValue()
+ {
+ var target = new Class1();
+ var source = new Subject();
+
+ target.Bind(Class1.QuxProperty, new TestOneTimeBinding(source));
+
+ source.OnNext(AvaloniaProperty.UnsetValue);
+ Assert.Equal(5.6, target.GetValue(Class1.QuxProperty));
+
+ source.OnNext(6.7);
+ Assert.Equal(6.7, target.GetValue(Class1.QuxProperty));
+ }
+
+ [Fact]
+ public void OneTime_Binding_Ignores_Binding_Errors()
+ {
+ var target = new Class1();
+ var source = new Subject();
+
+ target.Bind(Class1.QuxProperty, new TestOneTimeBinding(source));
+
+ source.OnNext(new BindingNotification(new Exception(), BindingErrorType.Error));
+ Assert.Equal(5.6, target.GetValue(Class1.QuxProperty));
+
+ source.OnNext(6.7);
+ Assert.Equal(6.7, target.GetValue(Class1.QuxProperty));
+ }
+
[Fact]
public void Bind_Throws_Exception_For_Unregistered_Property()
{
@@ -273,31 +303,36 @@ namespace Avalonia.Base.UnitTests
target.Bind(Class1.QuxProperty, source);
source.OnNext(6.7);
- source.OnNext(new BindingError(new InvalidOperationException("Foo")));
+ source.OnNext(new BindingNotification(
+ new InvalidOperationException("Foo"),
+ BindingErrorType.Error));
Assert.Equal(6.7, target.GetValue(Class1.QuxProperty));
}
[Fact]
- public void BindingError_With_FallbackValue_Causes_Target_Update()
+ public void BindingNotification_With_FallbackValue_Causes_Target_Update()
{
var target = new Class1();
var source = new Subject();
target.Bind(Class1.QuxProperty, source);
source.OnNext(6.7);
- source.OnNext(new BindingError(new InvalidOperationException("Foo"), 8.9));
+ source.OnNext(new BindingNotification(
+ new InvalidOperationException("Foo"),
+ BindingErrorType.Error,
+ 8.9));
Assert.Equal(8.9, target.GetValue(Class1.QuxProperty));
}
[Fact]
- public void Bind_Logs_BindingError()
+ public void Bind_Logs_Binding_Error()
{
var target = new Class1();
var source = new Subject();
var called = false;
- var expectedMessageTemplate = "Error binding to {Target}.{Property}: {Message}";
+ var expectedMessageTemplate = "Error in binding to {Target}.{Property}: {Message}";
LogCallback checkLogMessage = (level, area, src, mt, pv) =>
{
@@ -313,7 +348,9 @@ namespace Avalonia.Base.UnitTests
{
target.Bind(Class1.QuxProperty, source);
source.OnNext(6.7);
- source.OnNext(new BindingError(new InvalidOperationException("Foo")));
+ source.OnNext(new BindingNotification(
+ new InvalidOperationException("Foo"),
+ BindingErrorType.Error));
Assert.Equal(6.7, target.GetValue(Class1.QuxProperty));
Assert.True(called);
@@ -345,5 +382,24 @@ namespace Avalonia.Base.UnitTests
public static readonly StyledProperty BarProperty =
AvaloniaProperty.Register("Bar", "bardefault");
}
+
+ private class TestOneTimeBinding : IBinding
+ {
+ private IObservable _source;
+
+ public TestOneTimeBinding(IObservable source)
+ {
+ _source = source;
+ }
+
+ public InstancedBinding Initiate(
+ IAvaloniaObject target,
+ AvaloniaProperty targetProperty,
+ object anchor = null,
+ bool enableDataValidation = false)
+ {
+ return new InstancedBinding(_source, BindingMode.OneTime);
+ }
+ }
}
}
diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs
new file mode 100644
index 0000000000..c8436c376f
--- /dev/null
+++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs
@@ -0,0 +1,143 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reactive.Subjects;
+using Avalonia.Data;
+using Xunit;
+
+namespace Avalonia.Base.UnitTests
+{
+ public class AvaloniaObjectTests_DataValidation
+ {
+ [Fact]
+ public void Setting_Non_Validated_Property_Does_Not_Call_UpdateDataValidation()
+ {
+ var target = new Class1();
+
+ target.SetValue(Class1.NonValidatedDirectProperty, 6);
+
+ Assert.Empty(target.Notifications);
+ }
+
+ [Fact]
+ public void Setting_Non_Validated_Direct_Property_Does_Not_Call_UpdateDataValidation()
+ {
+ var target = new Class1();
+
+ target.SetValue(Class1.NonValidatedDirectProperty, 6);
+
+ Assert.Empty(target.Notifications);
+ }
+
+ [Fact]
+ public void Setting_Validated_Direct_Property_Calls_UpdateDataValidation()
+ {
+ var target = new Class1();
+
+ target.SetValue(Class1.ValidatedDirectProperty, new BindingNotification(6));
+ target.SetValue(Class1.ValidatedDirectProperty, new BindingNotification(new Exception(), BindingErrorType.Error));
+ target.SetValue(Class1.ValidatedDirectProperty, new BindingNotification(new Exception(), BindingErrorType.DataValidationError));
+ target.SetValue(Class1.ValidatedDirectProperty, new BindingNotification(7));
+
+ Assert.Equal(
+ new[]
+ {
+ new BindingNotification(6),
+ new BindingNotification(new Exception(), BindingErrorType.Error),
+ new BindingNotification(new Exception(), BindingErrorType.DataValidationError),
+ new BindingNotification(7),
+ },
+ target.Notifications.AsEnumerable());
+ }
+
+ [Fact]
+ public void Binding_Non_Validated_Property_Does_Not_Call_UpdateDataValidation()
+ {
+ var source = new Subject();
+ var target = new Class1
+ {
+ [!Class1.NonValidatedProperty] = source.AsBinding(),
+ };
+
+ source.OnNext(new BindingNotification(6));
+ source.OnNext(new BindingNotification(new Exception(), BindingErrorType.Error));
+ source.OnNext(new BindingNotification(new Exception(), BindingErrorType.DataValidationError));
+ source.OnNext(new BindingNotification(7));
+
+ Assert.Empty(target.Notifications);
+ }
+
+ [Fact]
+ public void Binding_Validated_Direct_Property_Calls_UpdateDataValidation()
+ {
+ var source = new Subject();
+ var target = new Class1
+ {
+ [!Class1.ValidatedDirectProperty] = source.AsBinding(),
+ };
+
+ source.OnNext(new BindingNotification(6));
+ source.OnNext(new BindingNotification(new Exception(), BindingErrorType.Error));
+ source.OnNext(new BindingNotification(new Exception(), BindingErrorType.DataValidationError));
+ source.OnNext(new BindingNotification(7));
+
+ Assert.Equal(
+ new[]
+ {
+ new BindingNotification(6),
+ new BindingNotification(new Exception(), BindingErrorType.Error),
+ new BindingNotification(new Exception(), BindingErrorType.DataValidationError),
+ new BindingNotification(7),
+ },
+ target.Notifications.AsEnumerable());
+ }
+
+ private class Class1 : AvaloniaObject
+ {
+ public static readonly StyledProperty NonValidatedProperty =
+ AvaloniaProperty.Register(
+ nameof(NonValidated));
+
+ public static readonly DirectProperty NonValidatedDirectProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(NonValidatedDirect),
+ o => o.NonValidatedDirect,
+ (o, v) => o.NonValidatedDirect = v);
+
+ public static readonly DirectProperty ValidatedDirectProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(ValidatedDirect),
+ o => o.ValidatedDirect,
+ (o, v) => o.ValidatedDirect = v,
+ enableDataValidation: true);
+
+ private int _nonValidatedDirect;
+ private int _direct;
+
+ public int NonValidated
+ {
+ get { return GetValue(NonValidatedProperty); }
+ set { SetValue(NonValidatedProperty, value); }
+ }
+
+ public int NonValidatedDirect
+ {
+ get { return _direct; }
+ set { SetAndRaise(NonValidatedDirectProperty, ref _nonValidatedDirect, value); }
+ }
+
+ public int ValidatedDirect
+ {
+ get { return _direct; }
+ set { SetAndRaise(ValidatedDirectProperty, ref _direct, value); }
+ }
+
+ public IList Notifications { get; } = new List();
+
+ protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification notification)
+ {
+ Notifications.Add(notification);
+ }
+ }
+ }
+}
diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs
index 1d8bb80cba..cbeb8765c3 100644
--- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs
+++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs
@@ -284,7 +284,6 @@ namespace Avalonia.Base.UnitTests
Assert.Equal("newvalue", target.Foo);
}
-
[Fact]
public void UnsetValue_Is_Used_On_AddOwnered_Property()
{
@@ -360,7 +359,7 @@ namespace Avalonia.Base.UnitTests
target.Bind(Class1.FooProperty, source);
source.OnNext("initial");
- source.OnNext(new BindingError(new InvalidOperationException("Foo")));
+ source.OnNext(new BindingNotification(new InvalidOperationException("Foo"), BindingErrorType.Error));
Assert.Equal("initial", target.GetValue(Class1.FooProperty));
}
@@ -373,7 +372,10 @@ namespace Avalonia.Base.UnitTests
target.Bind(Class1.FooProperty, source);
source.OnNext("initial");
- source.OnNext(new BindingError(new InvalidOperationException("Foo"), "fallback"));
+ source.OnNext(new BindingNotification(
+ new InvalidOperationException("Foo"),
+ BindingErrorType.Error,
+ "fallback"));
Assert.Equal("fallback", target.GetValue(Class1.FooProperty));
}
@@ -389,7 +391,7 @@ namespace Avalonia.Base.UnitTests
{
if (level == LogEventLevel.Error &&
area == LogArea.Binding &&
- mt == "Error binding to {Target}.{Property}: {Message}" &&
+ mt == "Error in binding to {Target}.{Property}: {Message}" &&
pv.Length == 3 &&
pv[0] is Class1 &&
object.ReferenceEquals(pv[1], Class1.FooProperty) &&
@@ -403,7 +405,7 @@ namespace Avalonia.Base.UnitTests
{
target.Bind(Class1.FooProperty, source);
source.OnNext("baz");
- source.OnNext(new BindingError(new InvalidOperationException("Binding Error Message")));
+ source.OnNext(new BindingNotification(new InvalidOperationException("Binding Error Message"), BindingErrorType.Error));
}
Assert.True(called);
diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs
index 4c15f9e676..da0b0252a3 100644
--- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs
+++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs
@@ -25,7 +25,7 @@ namespace Avalonia.Base.UnitTests
.Select(x => x.Name)
.ToArray();
- Assert.Equal(new[] { "Foo", "Baz", "Qux", "Attached", "ValidationStatus" }, names);
+ Assert.Equal(new[] { "Foo", "Baz", "Qux", "Attached" }, names);
}
[Fact]
@@ -35,7 +35,7 @@ namespace Avalonia.Base.UnitTests
.Select(x => x.Name)
.ToArray();
- Assert.Equal(new[] { "Bar", "Flob", "Fred", "Foo", "Baz", "Qux", "Attached", "ValidationStatus" }, names);
+ Assert.Equal(new[] { "Bar", "Flob", "Fred", "Foo", "Baz", "Qux", "Attached" }, names);
}
[Fact]
diff --git a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj
index ef831b27fd..e46b43d90e 100644
--- a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj
+++ b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj
@@ -98,7 +98,7 @@
-
+
diff --git a/tests/Avalonia.Controls.UnitTests/DropDownTests.cs b/tests/Avalonia.Controls.UnitTests/DropDownTests.cs
index fc866e5f86..30cca90b4a 100644
--- a/tests/Avalonia.Controls.UnitTests/DropDownTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/DropDownTests.cs
@@ -1,20 +1,65 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
-using System;
using System.Linq;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Shapes;
using Avalonia.Controls.Templates;
-using Avalonia.Platform;
-using Ploeh.AutoFixture;
-using Ploeh.AutoFixture.AutoMoq;
+using Avalonia.LogicalTree;
+using Avalonia.Media;
+using Avalonia.UnitTests;
+using Avalonia.VisualTree;
using Xunit;
namespace Avalonia.Controls.UnitTests
{
public class DropDownTests
{
+ [Fact]
+ public void SelectionBoxItem_Is_Rectangle_With_VisualBrush_When_Selection_Is_Control()
+ {
+ var items = new[] { new Canvas() };
+ var target = new DropDown
+ {
+ Items = items,
+ SelectedIndex = 0,
+ };
+
+ var rectangle = target.GetValue(DropDown.SelectionBoxItemProperty) as Rectangle;
+ Assert.NotNull(rectangle);
+
+ var brush = rectangle.Fill as VisualBrush;
+ Assert.NotNull(brush);
+ Assert.Same(items[0], brush.Visual);
+ }
+
+ [Fact]
+ public void SelectionBoxItem_Rectangle_Is_Removed_From_Logical_Tree()
+ {
+ var target = new DropDown
+ {
+ Items = new[] { new Canvas() },
+ SelectedIndex = 0,
+ Template = GetTemplate(),
+ };
+
+ var root = new TestRoot { Child = target };
+ target.ApplyTemplate();
+ target.Presenter.ApplyTemplate();
+
+ var rectangle = target.GetValue(DropDown.SelectionBoxItemProperty) as Rectangle;
+ Assert.True(((ILogical)target).IsAttachedToLogicalTree);
+ Assert.True(((ILogical)rectangle).IsAttachedToLogicalTree);
+
+ rectangle.DetachedFromLogicalTree += (s, e) => { };
+
+ root.Child = null;
+
+ Assert.False(((ILogical)target).IsAttachedToLogicalTree);
+ Assert.False(((ILogical)rectangle).IsAttachedToLogicalTree);
+ }
+
private FuncControlTemplate GetTemplate()
{
return new FuncControlTemplate(parent =>
@@ -26,8 +71,7 @@ namespace Avalonia.Controls.UnitTests
{
new ContentControl
{
- Name = "contentControl",
- [~ContentPresenter.ContentProperty] = parent[~DropDown.SelectionBoxItemProperty],
+ [!ContentControl.ContentProperty] = parent[!DropDown.SelectionBoxItemProperty],
},
new ToggleButton
{
@@ -35,7 +79,12 @@ namespace Avalonia.Controls.UnitTests
},
new Popup
{
- Name = "popup",
+ Name = "PART_Popup",
+ Child = new ItemsPresenter
+ {
+ Name = "PART_ItemsPresenter",
+ [!ItemsPresenter.ItemsProperty] = parent[!DropDown.ItemsProperty],
+ }
}
}
};
diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
index 5581087b5f..13d97920ee 100644
--- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
@@ -166,6 +166,24 @@ namespace Avalonia.Controls.UnitTests.Primitives
}
}
+ [Fact]
+ public void PopupRoot_Should_Be_Detached_From_Logical_Tree_When_Popup_Is_Detached()
+ {
+ using (CreateServices())
+ {
+ var target = new Popup();
+ var root = new TestRoot { Child = target };
+
+ target.Open();
+
+ var popupRoot = (ILogical)target.PopupRoot;
+
+ Assert.True(popupRoot.IsAttachedToLogicalTree);
+ root.Child = null;
+ Assert.False(((ILogical)target).IsAttachedToLogicalTree);
+ }
+ }
+
[Fact]
public void PopupRoot_Should_Have_Template_Applied()
{
diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
index 5619311537..8f206cc016 100644
--- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
@@ -1,8 +1,11 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
+using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
+using Avalonia.Markup.Xaml.Data;
using Avalonia.Platform;
using Avalonia.UnitTests;
using Moq;
@@ -21,6 +24,52 @@ namespace Avalonia.Controls.UnitTests
}
[Fact]
+ public void Typing_Beginning_With_0_Should_Not_Modify_Text_When_Bound_To_Int()
+ {
+ using (UnitTestApplication.Start(Services))
+ {
+ var source = new Class1();
+ var target = new TextBox
+ {
+ DataContext = source,
+ Template = CreateTemplate(),
+ };
+
+ target.ApplyTemplate();
+ target.Bind(TextBox.TextProperty, new Binding(nameof(Class1.Foo), BindingMode.TwoWay));
+
+ Assert.Equal("0", target.Text);
+
+ target.CaretIndex = 1;
+ target.RaiseEvent(new TextInputEventArgs
+ {
+ RoutedEvent = InputElement.TextInputEvent,
+ Text = "2",
+ });
+
+ Assert.Equal("02", target.Text);
+ }
+ }
+
+ private static TestServices Services => TestServices.MockThreadingInterface.With(
+ standardCursorFactory: Mock.Of());
+
+ private IControlTemplate CreateTemplate()
+ {
+ return new FuncControlTemplate(control =>
+ new TextPresenter
+ {
+ Name = "PART_TextPresenter",
+ [!!TextPresenter.TextProperty] = new Binding
+ {
+ Path = "Text",
+ Mode = BindingMode.TwoWay,
+ Priority = BindingPriority.TemplatedParent,
+ RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),
+ },
+ });
+ }
+
public void Control_Backspace_Should_Remove_The_Word_Before_The_Caret_If_There_Is_No_Selection()
{
AvaloniaLocator.CurrentMutable
@@ -109,5 +158,16 @@ namespace Avalonia.Controls.UnitTests
Key = key
});
}
+
+ private class Class1 : NotifyingBase
+ {
+ private int _foo;
+
+ public int Foo
+ {
+ get { return _foo; }
+ set { _foo = value; RaisePropertyChanged(); }
+ }
+ }
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests_ValidationState.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs
similarity index 51%
rename from tests/Avalonia.Controls.UnitTests/TextBoxTests_ValidationState.cs
rename to tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs
index 6ff3629acf..ff3634b9fe 100644
--- a/tests/Avalonia.Controls.UnitTests/TextBoxTests_ValidationState.cs
+++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs
@@ -5,71 +5,84 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
+using System.Linq;
+using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Templates;
+using Avalonia.Data;
using Avalonia.Markup.Xaml.Data;
+using Avalonia.Platform;
using Avalonia.UnitTests;
+using Moq;
using Xunit;
namespace Avalonia.Controls.UnitTests
{
- public class TextBoxTests_ValidationState
+ public class TextBoxTests_DataValidation
{
[Fact]
- public void Setter_Exceptions_Should_Set_ValidationState()
+ public void Setter_Exceptions_Should_Set_Error_Pseudoclass()
{
- using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
+ using (UnitTestApplication.Start(Services))
{
- var target = new TextBox();
- var binding = new Binding(nameof(ExceptionTest.LessThan10));
- binding.Source = new ExceptionTest();
- binding.EnableValidation = true;
- target.Bind(TextBox.TextProperty, binding);
+ var target = new TextBox
+ {
+ DataContext = new ExceptionTest(),
+ [!TextBox.TextProperty] = new Binding(nameof(ExceptionTest.LessThan10), BindingMode.TwoWay),
+ Template = CreateTemplate(),
+ };
- Assert.True(target.ValidationStatus.IsValid);
- target.Text = "20";
- Assert.False(target.ValidationStatus.IsValid);
- target.Text = "1";
- Assert.True(target.ValidationStatus.IsValid);
- }
- }
+ target.ApplyTemplate();
- [Fact(Skip = "TODO: Not yet passing")]
- public void Unconvertable_Value_Should_Set_ValidationState()
- {
- using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
- {
- var target = new TextBox();
- var binding = new Binding(nameof(ExceptionTest.LessThan10));
- binding.Source = new ExceptionTest();
- binding.EnableValidation = true;
- target.Bind(TextBox.TextProperty, binding);
-
- Assert.True(target.ValidationStatus.IsValid);
- target.Text = "foo";
- Assert.False(target.ValidationStatus.IsValid);
+ Assert.False(target.Classes.Contains(":error"));
+ target.Text = "20";
+ Assert.True(target.Classes.Contains(":error"));
target.Text = "1";
- Assert.True(target.ValidationStatus.IsValid);
+ Assert.False(target.Classes.Contains(":error"));
}
}
[Fact]
- public void Indei_Should_Set_ValidationState()
+ public void Setter_Exceptions_Should_Set_DataValidationErrors()
{
- using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
+ using (UnitTestApplication.Start(Services))
{
- var target = new TextBox();
- var binding = new Binding(nameof(ExceptionTest.LessThan10));
- binding.Source = new IndeiTest();
- binding.EnableValidation = true;
- target.Bind(TextBox.TextProperty, binding);
+ var target = new TextBox
+ {
+ DataContext = new ExceptionTest(),
+ [!TextBox.TextProperty] = new Binding(nameof(ExceptionTest.LessThan10), BindingMode.TwoWay),
+ Template = CreateTemplate(),
+ };
+
+ target.ApplyTemplate();
- Assert.True(target.ValidationStatus.IsValid);
+ Assert.Null(target.DataValidationErrors);
target.Text = "20";
- Assert.False(target.ValidationStatus.IsValid);
+ Assert.Equal(1, target.DataValidationErrors.Count());
+ Assert.IsType(target.DataValidationErrors.Single());
target.Text = "1";
- Assert.True(target.ValidationStatus.IsValid);
+ Assert.Null(target.DataValidationErrors);
}
}
+ private static TestServices Services => TestServices.MockThreadingInterface.With(
+ standardCursorFactory: Mock.Of());
+
+ private IControlTemplate CreateTemplate()
+ {
+ return new FuncControlTemplate(control =>
+ new TextPresenter
+ {
+ Name = "PART_TextPresenter",
+ [!!TextPresenter.TextProperty] = new Binding
+ {
+ Path = "Text",
+ Mode = BindingMode.TwoWay,
+ Priority = BindingPriority.TemplatedParent,
+ RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),
+ },
+ });
+ }
+
private class ExceptionTest
{
private int _lessThan10;
diff --git a/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs b/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs
index a97e4a2140..a9c991b8b9 100644
--- a/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs
+++ b/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs
@@ -20,7 +20,7 @@ namespace Avalonia.Controls.UnitTests
return _windowImpl?.Invoke() ?? Mock.Of(x => x.Scaling == 1);
}
- public IWindowImpl CreateEmbeddableWindow()
+ public IEmbeddableWindowImpl CreateEmbeddableWindow()
{
throw new NotImplementedException();
}
diff --git a/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj b/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj
index 587deb4864..9aef304beb 100644
--- a/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj
+++ b/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj
@@ -98,6 +98,7 @@
+
diff --git a/tests/Avalonia.LeakTests/ExpressionObserverTests.cs b/tests/Avalonia.LeakTests/ExpressionObserverTests.cs
new file mode 100644
index 0000000000..3dbc62424f
--- /dev/null
+++ b/tests/Avalonia.LeakTests/ExpressionObserverTests.cs
@@ -0,0 +1,92 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Collections;
+using Avalonia.Markup.Data;
+using Avalonia.UnitTests;
+using JetBrains.dotMemoryUnit;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Avalonia.LeakTests
+{
+ [DotMemoryUnit(FailIfRunWithoutSupport = false)]
+ public class ExpressionObserverTests
+ {
+ public ExpressionObserverTests(ITestOutputHelper atr)
+ {
+ DotMemoryUnitTestOutput.SetOutputMethod(atr.WriteLine);
+ }
+
+ [Fact]
+ public void Should_Not_Keep_Source_Alive_ObservableCollection()
+ {
+ Func run = () =>
+ {
+ var source = new { Foo = new AvaloniaList {"foo", "bar"} };
+ var target = new ExpressionObserver(source, "Foo");
+
+ target.Subscribe(_ => { });
+ return target;
+ };
+
+ var result = run();
+
+ dotMemory.Check(memory =>
+ Assert.Equal(0, memory.GetObjects(where => where.Type.Is>()).ObjectsCount));
+ }
+
+ [Fact]
+ public void Should_Not_Keep_Source_Alive_ObservableCollection_With_DataValidation()
+ {
+ Func run = () =>
+ {
+ var source = new { Foo = new AvaloniaList { "foo", "bar" } };
+ var target = new ExpressionObserver(source, "Foo", true);
+
+ target.Subscribe(_ => { });
+ return target;
+ };
+
+ var result = run();
+
+ dotMemory.Check(memory =>
+ Assert.Equal(0, memory.GetObjects(where => where.Type.Is>()).ObjectsCount));
+ }
+
+ [Fact]
+ public void Should_Not_Keep_Source_Alive_NonIntegerIndexer()
+ {
+ Func run = () =>
+ {
+ var source = new { Foo = new NonIntegerIndexer() };
+ var target = new ExpressionObserver(source, "Foo");
+
+ target.Subscribe(_ => { });
+ return target;
+ };
+
+ var result = run();
+
+ dotMemory.Check(memory =>
+ Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount));
+ }
+
+ private class NonIntegerIndexer : NotifyingBase
+ {
+ private readonly Dictionary _storage = new Dictionary();
+
+ public string this[string key]
+ {
+ get
+ {
+ return _storage[key];
+ }
+ set
+ {
+ _storage[key] = value;
+ RaisePropertyChanged(CommonPropertyNames.IndexerName);
+ }
+ }
+ }
+ }
+}
diff --git a/tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj b/tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj
index ae691bfc0f..66d234ac19 100644
--- a/tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj
+++ b/tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj
@@ -10,10 +10,11 @@
Properties
Avalonia.Markup.UnitTests
Avalonia.Markup.UnitTests
- v4.6
+ v4.5
512
+
true
@@ -34,7 +35,7 @@
- ..\..\packages\Microsoft.Reactive.Testing.3.0.0\lib\net46\Microsoft.Reactive.Testing.dll
+ ..\..\packages\Microsoft.Reactive.Testing.3.0.0\lib\net45\Microsoft.Reactive.Testing.dll
True
@@ -43,9 +44,13 @@
True
+
+ C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETPortable\v4.5\Profile\Profile7\System.ComponentModel.Annotations.dll
+
+
- ..\..\packages\System.Reactive.Core.3.0.0\lib\net46\System.Reactive.Core.dll
+ ..\..\packages\System.Reactive.Core.3.0.0\lib\net45\System.Reactive.Core.dll
True
@@ -53,11 +58,15 @@
True
- ..\..\packages\System.Reactive.Linq.3.0.0\lib\net46\System.Reactive.Linq.dll
+ ..\..\packages\System.Reactive.Linq.3.0.0\lib\net45\System.Reactive.Linq.dll
True
- ..\..\packages\System.Reactive.PlatformServices.3.0.0\lib\net46\System.Reactive.PlatformServices.dll
+ ..\..\packages\System.Reactive.PlatformServices.3.0.0\lib\net45\System.Reactive.PlatformServices.dll
+ True
+
+
+ ..\..\packages\System.Reactive.Windows.Threading.3.0.0\lib\net45\System.Reactive.Windows.Threading.dll
True
@@ -66,6 +75,7 @@
+
..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll
True
@@ -85,7 +95,10 @@
-
+
+
+
+
@@ -97,9 +110,8 @@
-
-
-
+
+
diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs
new file mode 100644
index 0000000000..c53dc417b0
--- /dev/null
+++ b/tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs
@@ -0,0 +1,320 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Reactive.Linq;
+using System.Threading;
+using Avalonia.Data;
+using Avalonia.Markup.Data;
+using Avalonia.UnitTests;
+using Moq;
+using Xunit;
+
+namespace Avalonia.Markup.UnitTests.Data
+{
+ public class BindingExpressionTests
+ {
+ [Fact]
+ public async void Should_Get_Simple_Property_Value()
+ {
+ var data = new Class1 { StringValue = "foo" };
+ var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(string));
+ var result = await target.Take(1);
+
+ Assert.Equal("foo", result);
+ }
+
+ [Fact]
+ public void Should_Set_Simple_Property_Value()
+ {
+ var data = new Class1 { StringValue = "foo" };
+ var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(string));
+
+ target.OnNext("bar");
+
+ Assert.Equal("bar", data.StringValue);
+ }
+
+ [Fact]
+ public async void Should_Convert_Get_String_To_Double()
+ {
+ Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
+
+ var data = new Class1 { StringValue = "5.6" };
+ var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double));
+ var result = await target.Take(1);
+
+ Assert.Equal(5.6, result);
+ }
+
+ [Fact]
+ public async void Getting_Invalid_Double_String_Should_Return_BindingError()
+ {
+ var data = new Class1 { StringValue = "foo" };
+ var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double));
+ var result = await target.Take(1);
+
+ Assert.IsType(result);
+ }
+
+ [Fact]
+ public async void Should_Coerce_Get_Null_Double_String_To_UnsetValue()
+ {
+ var data = new Class1 { StringValue = null };
+ var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double));
+ var result = await target.Take(1);
+
+ Assert.Equal(AvaloniaProperty.UnsetValue, result);
+ }
+
+ [Fact]
+ public void Should_Convert_Set_String_To_Double()
+ {
+ Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
+
+ var data = new Class1 { StringValue = (5.6).ToString() };
+ var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double));
+
+ target.OnNext(6.7);
+
+ Assert.Equal((6.7).ToString(), data.StringValue);
+ }
+
+ [Fact]
+ public async void Should_Convert_Get_Double_To_String()
+ {
+ Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
+
+ var data = new Class1 { DoubleValue = 5.6 };
+ var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string));
+ var result = await target.Take(1);
+
+ Assert.Equal((5.6).ToString(), result);
+ }
+
+ [Fact]
+ public void Should_Convert_Set_Double_To_String()
+ {
+ Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
+
+ var data = new Class1 { DoubleValue = 5.6 };
+ var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string));
+
+ target.OnNext("6.7");
+
+ Assert.Equal(6.7, data.DoubleValue);
+ }
+
+ [Fact]
+ public async void Should_Return_BindingNotification_With_FallbackValue_For_NonConvertibe_Target_Value()
+ {
+ Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
+
+ var data = new Class1 { StringValue = "foo" };
+ var target = new BindingExpression(
+ new ExpressionObserver(data, "StringValue"),
+ typeof(int),
+ 42,
+ DefaultValueConverter.Instance);
+ var result = await target.Take(1);
+
+ Assert.Equal(
+ new BindingNotification(
+ new InvalidCastException("'foo' is not a valid number."),
+ BindingErrorType.Error,
+ 42),
+ result);
+ }
+
+ [Fact]
+ public async void Should_Return_BindingNotification_With_FallbackValue_For_NonConvertibe_Target_Value_With_Data_Validation()
+ {
+ Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
+
+ var data = new Class1 { StringValue = "foo" };
+ var target = new BindingExpression(
+ new ExpressionObserver(data, "StringValue", true),
+ typeof(int),
+ 42,
+ DefaultValueConverter.Instance);
+ var result = await target.Take(1);
+
+ Assert.Equal(
+ new BindingNotification(
+ new InvalidCastException("'foo' is not a valid number."),
+ BindingErrorType.Error,
+ 42),
+ result);
+ }
+
+ [Fact]
+ public async void Should_Return_BindingNotification_For_Invalid_FallbackValue()
+ {
+ Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
+
+ var data = new Class1 { StringValue = "foo" };
+ var target = new BindingExpression(
+ new ExpressionObserver(data, "StringValue"),
+ typeof(int),
+ "bar",
+ DefaultValueConverter.Instance);
+ var result = await target.Take(1);
+
+ Assert.Equal(
+ new BindingNotification(
+ new AggregateException(
+ new InvalidCastException("Could not convert 'foo' to 'System.Int32'"),
+ new InvalidCastException("Could not convert FallbackValue 'bar' to 'System.Int32'")),
+ BindingErrorType.Error),
+ result);
+ }
+
+ [Fact]
+ public async void Should_Return_BindingNotification_For_Invalid_FallbackValue_With_Data_Validation()
+ {
+ Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
+
+ var data = new Class1 { StringValue = "foo" };
+ var target = new BindingExpression(
+ new ExpressionObserver(data, "StringValue", true),
+ typeof(int),
+ "bar",
+ DefaultValueConverter.Instance);
+ var result = await target.Take(1);
+
+ Assert.Equal(
+ new BindingNotification(
+ new AggregateException(
+ new InvalidCastException("Could not convert 'foo' to 'System.Int32'"),
+ new InvalidCastException("Could not convert FallbackValue 'bar' to 'System.Int32'")),
+ BindingErrorType.Error),
+ result);
+ }
+
+ [Fact]
+ public void Setting_Invalid_Double_String_Should_Not_Change_Target()
+ {
+ var data = new Class1 { DoubleValue = 5.6 };
+ var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string));
+
+ target.OnNext("foo");
+
+ Assert.Equal(5.6, data.DoubleValue);
+ }
+
+ [Fact]
+ public void Setting_Invalid_Double_String_Should_Use_FallbackValue()
+ {
+ var data = new Class1 { DoubleValue = 5.6 };
+ var target = new BindingExpression(
+ new ExpressionObserver(data, "DoubleValue"),
+ typeof(string),
+ "9.8",
+ DefaultValueConverter.Instance);
+
+ target.OnNext("foo");
+
+ Assert.Equal(9.8, data.DoubleValue);
+ }
+
+ [Fact]
+ public void Should_Coerce_Setting_Null_Double_To_Default_Value()
+ {
+ var data = new Class1 { DoubleValue = 5.6 };
+ var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string));
+
+ target.OnNext(null);
+
+ Assert.Equal(0, data.DoubleValue);
+ }
+
+ [Fact]
+ public void Should_Coerce_Setting_UnsetValue_Double_To_Default_Value()
+ {
+ var data = new Class1 { DoubleValue = 5.6 };
+ var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string));
+
+ target.OnNext(AvaloniaProperty.UnsetValue);
+
+ Assert.Equal(0, data.DoubleValue);
+ }
+
+ [Fact]
+ public void Should_Pass_ConverterParameter_To_Convert()
+ {
+ var data = new Class1 { DoubleValue = 5.6 };
+ var converter = new Mock();
+ var target = new BindingExpression(
+ new ExpressionObserver(data, "DoubleValue"),
+ typeof(string),
+ converter.Object,
+ converterParameter: "foo");
+
+ target.Subscribe(_ => { });
+
+ converter.Verify(x => x.Convert(5.6, typeof(string), "foo", CultureInfo.CurrentUICulture));
+ }
+
+ [Fact]
+ public void Should_Pass_ConverterParameter_To_ConvertBack()
+ {
+ var data = new Class1 { DoubleValue = 5.6 };
+ var converter = new Mock();
+ var target = new BindingExpression(
+ new ExpressionObserver(data, "DoubleValue"),
+ typeof(string),
+ converter.Object,
+ converterParameter: "foo");
+
+ target.OnNext("bar");
+
+ converter.Verify(x => x.ConvertBack("bar", typeof(double), "foo", CultureInfo.CurrentUICulture));
+ }
+
+ [Fact]
+ public void Should_Handle_DataValidation()
+ {
+ var data = new Class1 { DoubleValue = 5.6 };
+ var converter = new Mock();
+ var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue", true), typeof(string));
+ var result = new List();
+
+ target.Subscribe(x => result.Add(x));
+ target.OnNext(1.2);
+ target.OnNext("3.4");
+ target.OnNext("bar");
+
+ Assert.Equal(
+ new[]
+ {
+ new BindingNotification("5.6"),
+ new BindingNotification("1.2"),
+ new BindingNotification("3.4"),
+ new BindingNotification(
+ new InvalidCastException("'bar' is not a valid number."),
+ BindingErrorType.Error)
+ },
+ result);
+ }
+
+ private class Class1 : NotifyingBase
+ {
+ private string _stringValue;
+ private double _doubleValue;
+
+ public string StringValue
+ {
+ get { return _stringValue; }
+ set { _stringValue = value; RaisePropertyChanged(); }
+ }
+
+ public double DoubleValue
+ {
+ get { return _doubleValue; }
+ set { _doubleValue = value; RaisePropertyChanged(); }
+ }
+ }
+ }
+}
diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExceptionValidatorTests.cs b/tests/Avalonia.Markup.UnitTests/Data/ExceptionValidatorTests.cs
deleted file mode 100644
index 6ff336c5a6..0000000000
--- a/tests/Avalonia.Markup.UnitTests/Data/ExceptionValidatorTests.cs
+++ /dev/null
@@ -1,93 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.ComponentModel;
-using System.Runtime.CompilerServices;
-using Avalonia.Data;
-using Avalonia.Markup.Data.Plugins;
-using Xunit;
-
-namespace Avalonia.Markup.UnitTests.Data
-{
- public class ExceptionValidatorTests
- {
- public class Data : INotifyPropertyChanged
- {
- private int nonValidated;
-
- public int NonValidated
- {
- get { return nonValidated; }
- set { nonValidated = value; NotifyPropertyChanged(); }
- }
-
- private int mustBePositive;
-
- public int MustBePositive
- {
- get { return mustBePositive; }
- set
- {
- if (value <= 0)
- {
- throw new ArgumentOutOfRangeException(nameof(value));
- }
- mustBePositive = value;
- }
- }
-
- public event PropertyChangedEventHandler PropertyChanged;
-
- private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
- {
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
- }
- }
-
- [Fact]
- public void Setting_Non_Validating_Triggers_Validation()
- {
- var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
- var validatorPlugin = new ExceptionValidationPlugin();
- var data = new Data();
- var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.NonValidated), _ => { });
- IValidationStatus status = null;
- var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.NonValidated), accessor, s => status = s);
-
- validator.SetValue(5, BindingPriority.LocalValue);
-
- Assert.NotNull(status);
- }
-
- [Fact]
- public void Setting_Validating_Property_To_Valid_Value_Returns_Successful_ValidationStatus()
- {
- var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
- var validatorPlugin = new ExceptionValidationPlugin();
- var data = new Data();
- var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), _ => { });
- IValidationStatus status = null;
- var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor, s => status = s);
-
- validator.SetValue(5, BindingPriority.LocalValue);
-
- Assert.True(status.IsValid);
- }
-
- [Fact]
- public void Setting_Validating_Property_To_Invalid_Value_Returns_Failed_ValidationStatus()
- {
- var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
- var validatorPlugin = new ExceptionValidationPlugin();
- var data = new Data();
- var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), _ => { });
- IValidationStatus status = null;
- var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor, s => status = s);
-
- validator.SetValue(-5, BindingPriority.LocalValue);
-
- Assert.False(status.IsValid);
- }
- }
-}
diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_DataValidation.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_DataValidation.cs
new file mode 100644
index 0000000000..fb98144647
--- /dev/null
+++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_DataValidation.cs
@@ -0,0 +1,222 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reactive.Linq;
+using Avalonia.Data;
+using Avalonia.Markup.Data;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Markup.UnitTests.Data
+{
+ public class ExpressionObserverTests_DataValidation
+ {
+ [Fact]
+ public void Doesnt_Send_DataValidationError_When_DataValidatation_Not_Enabled()
+ {
+ var data = new ExceptionTest { MustBePositive = 5 };
+ var observer = new ExpressionObserver(data, nameof(data.MustBePositive), false);
+ var validationMessageFound = false;
+
+ observer.OfType()
+ .Where(x => x.ErrorType == BindingErrorType.DataValidationError)
+ .Subscribe(_ => validationMessageFound = true);
+ observer.SetValue(-5);
+
+ Assert.False(validationMessageFound);
+ }
+
+ [Fact]
+ public void Exception_Validation_Sends_DataValidationError()
+ {
+ var data = new ExceptionTest { MustBePositive = 5 };
+ var observer = new ExpressionObserver(data, nameof(data.MustBePositive), true);
+ var validationMessageFound = false;
+
+ observer.OfType()
+ .Where(x => x.ErrorType == BindingErrorType.DataValidationError)
+ .Subscribe(_ => validationMessageFound = true);
+ observer.SetValue(-5);
+
+ Assert.True(validationMessageFound);
+ }
+
+ [Fact]
+ public void Indei_Validation_Does_Not_Subscribe_When_DataValidatation_Not_Enabled()
+ {
+ var data = new IndeiTest { MustBePositive = 5 };
+ var observer = new ExpressionObserver(data, nameof(data.MustBePositive), false);
+
+ observer.Subscribe(_ => { });
+
+ Assert.Equal(0, data.ErrorsChangedSubscriptionCount);
+ }
+
+ [Fact]
+ public void Enabled_Indei_Validation_Subscribes()
+ {
+ var data = new IndeiTest { MustBePositive = 5 };
+ var observer = new ExpressionObserver(data, nameof(data.MustBePositive), true);
+ var sub = observer.Subscribe(_ => { });
+
+ Assert.Equal(1, data.ErrorsChangedSubscriptionCount);
+ sub.Dispose();
+ Assert.Equal(0, data.ErrorsChangedSubscriptionCount);
+ }
+
+ [Fact]
+ public void Validation_Plugins_Send_Correct_Notifications()
+ {
+ var data = new IndeiTest();
+ var observer = new ExpressionObserver(data, nameof(data.MustBePositive), true);
+ var result = new List();
+
+ observer.Subscribe(x => result.Add(x));
+ observer.SetValue(5);
+ observer.SetValue(-5);
+ observer.SetValue("foo");
+ observer.SetValue(5);
+
+ Assert.Equal(new[]
+ {
+ new BindingNotification(0),
+
+ // Value is notified twice as ErrorsChanged is always called by IndeiTest.
+ new BindingNotification(5),
+ new BindingNotification(5),
+
+ // Value is first signalled without an error as validation hasn't been updated.
+ new BindingNotification(-5),
+ new BindingNotification(new Exception("Must be positive"), BindingErrorType.DataValidationError, -5),
+
+ // Exception is thrown by trying to set value to "foo".
+ new BindingNotification(
+ new ArgumentException("Object of type 'System.String' cannot be converted to type 'System.Int32'."),
+ BindingErrorType.DataValidationError),
+
+ // Value is set then validation is updated.
+ new BindingNotification(new Exception("Must be positive"), BindingErrorType.DataValidationError, 5),
+ new BindingNotification(5),
+ }, result);
+ }
+
+ [Fact]
+ public void Doesnt_Subscribe_To_Indei_Of_Intermediate_Object_In_Chain()
+ {
+ var data = new Container
+ {
+ Inner = new IndeiTest()
+ };
+
+ var observer = new ExpressionObserver(
+ data,
+ $"{nameof(Container.Inner)}.{nameof(IndeiTest.MustBePositive)}",
+ true);
+
+ observer.Subscribe(_ => { });
+
+ // We may want to change this but I've never seen an example of data validation on an
+ // intermediate object in a chain so for the moment I'm not sure what the result of
+ // validating such a thing should look like.
+ Assert.Equal(0, data.ErrorsChangedSubscriptionCount);
+ Assert.Equal(1, ((IndeiTest)data.Inner).ErrorsChangedSubscriptionCount);
+ }
+
+ [Fact]
+ public void Sends_Correct_Notifications_With_Property_Chain()
+ {
+ var container = new Container();
+ var inner = new IndeiTest();
+
+ var observer = new ExpressionObserver(
+ container,
+ $"{nameof(Container.Inner)}.{nameof(IndeiTest.MustBePositive)}",
+ true);
+ var result = new List();
+
+ observer.Subscribe(x => result.Add(x));
+
+ Assert.Equal(new[]
+ {
+ new BindingNotification(
+ new MarkupBindingChainNullException("Inner.MustBePositive", "Inner"),
+ BindingErrorType.Error,
+ AvaloniaProperty.UnsetValue),
+ }, result);
+ }
+
+ public class ExceptionTest : NotifyingBase
+ {
+ private int _mustBePositive;
+
+ public int MustBePositive
+ {
+ get { return _mustBePositive; }
+ set
+ {
+ if (value <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+
+ _mustBePositive = value;
+ RaisePropertyChanged();
+ }
+ }
+ }
+
+ private class IndeiTest : IndeiBase
+ {
+ private int _mustBePositive;
+ private Dictionary> _errors = new Dictionary>();
+
+ public int MustBePositive
+ {
+ get { return _mustBePositive; }
+ set
+ {
+ _mustBePositive = value;
+ RaisePropertyChanged();
+
+ if (value >= 0)
+ {
+ _errors.Remove(nameof(MustBePositive));
+ RaiseErrorsChanged(nameof(MustBePositive));
+ }
+ else
+ {
+ _errors[nameof(MustBePositive)] = new[] { "Must be positive" };
+ RaiseErrorsChanged(nameof(MustBePositive));
+ }
+ }
+ }
+
+ public override bool HasErrors => _mustBePositive >= 0;
+
+ public override IEnumerable GetErrors(string propertyName)
+ {
+ IList result;
+ _errors.TryGetValue(propertyName, out result);
+ return result;
+ }
+ }
+
+ private class Container : IndeiBase
+ {
+ private object _inner;
+
+ public object Inner
+ {
+ get { return _inner; }
+ set { _inner = value; RaisePropertyChanged(); }
+ }
+
+ public override bool HasErrors => false;
+ public override IEnumerable GetErrors(string propertyName) => null;
+ }
+ }
+}
diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs
index b79498baae..f6c4540611 100644
--- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs
+++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs
@@ -189,58 +189,22 @@ namespace Avalonia.Markup.UnitTests.Data
var expected = new[] { "bar", "bar2" };
Assert.Equal(expected, result);
- Assert.Equal(0, data.Foo.SubscriptionCount);
- }
-
- [Fact]
- public void Should_Not_Keep_Source_Alive_ObservableCollection()
- {
- Func> run = () =>
- {
- var source = new { Foo = new AvaloniaList { "foo", "bar" } };
- var target = new ExpressionObserver(source, "Foo");
- return Tuple.Create(target, new WeakReference(source.Foo));
- };
-
- var result = run();
- result.Item1.Subscribe(x => { });
-
- GC.Collect();
-
- Assert.Null(result.Item2.Target);
- }
-
- [Fact]
- public void Should_Not_Keep_Source_Alive_NonIntegerIndexer()
- {
- Func> run = () =>
- {
- var source = new NonIntegerIndexer();
- var target = new ExpressionObserver(source, "Foo");
- return Tuple.Create(target, new WeakReference(source));
- };
-
- var result = run();
- result.Item1.Subscribe(x => { });
-
- GC.Collect();
-
- Assert.Null(result.Item2.Target);
+ Assert.Equal(0, data.Foo.PropertyChangedSubscriptionCount);
}
private class NonIntegerIndexer : NotifyingBase
{
- private Dictionary storage = new Dictionary();
+ private readonly Dictionary _storage = new Dictionary();
public string this[string key]
{
get
{
- return storage[key];
+ return _storage[key];
}
set
{
- storage[key] = value;
+ _storage[key] = value;
RaisePropertyChanged(CommonPropertyNames.IndexerName);
}
}
diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs
index 9fa753917c..2a2bf06bf1 100644
--- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs
+++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs
@@ -27,6 +27,19 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.True(completed);
}
+ [Fact]
+ public void Should_Complete_When_Source_Observable_Errors()
+ {
+ var source = new BehaviorSubject(1);
+ var target = new ExpressionObserver(source, "Foo");
+ var completed = false;
+
+ target.Subscribe(_ => { }, () => completed = true);
+ source.OnError(new Exception());
+
+ Assert.True(completed);
+ }
+
[Fact]
public void Should_Complete_When_Update_Observable_Completes()
{
@@ -40,6 +53,19 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.True(completed);
}
+ [Fact]
+ public void Should_Complete_When_Update_Observable_Errors()
+ {
+ var update = new Subject();
+ var target = new ExpressionObserver(() => 1, "Foo", update);
+ var completed = false;
+
+ target.Subscribe(_ => { }, () => completed = true);
+ update.OnError(new Exception());
+
+ Assert.True(completed);
+ }
+
[Fact]
public void Should_Unsubscribe_From_Source_Observable()
{
@@ -55,7 +81,7 @@ namespace Avalonia.Markup.UnitTests.Data
scheduler.Start();
}
- Assert.Equal(new[] { AvaloniaProperty.UnsetValue, "foo" }, result);
+ Assert.Equal(new[] { "foo" }, result);
Assert.All(source.Subscriptions, x => Assert.NotEqual(Subscription.Infinite, x.Unsubscribe));
}
@@ -77,22 +103,6 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.All(update.Subscriptions, x => Assert.NotEqual(Subscription.Infinite, x.Unsubscribe));
}
- [Fact]
- public void Should_Set_Node_Target_To_Null_On_Unsubscribe()
- {
- var target = new ExpressionObserver(new { Foo = "foo" }, "Foo");
- var result = new List();
-
- using (target.Subscribe(x => result.Add(x)))
- using (target.Subscribe(_ => { }))
- {
- Assert.NotNull(target.Node.Target);
- }
-
- Assert.Equal(new[] { "foo" }, result);
- Assert.Null(target.Node.Target);
- }
-
private Recorded> OnNext(long time, object value)
{
return new Recorded>(time, Notification.CreateOnNext(value));
diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Negation.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Negation.cs
index b3046118be..6bee0d10f4 100644
--- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Negation.cs
+++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Negation.cs
@@ -3,6 +3,7 @@
using System;
using System.Reactive.Linq;
+using Avalonia.Data;
using Avalonia.Markup.Data;
using Xunit;
@@ -61,23 +62,31 @@ namespace Avalonia.Markup.UnitTests.Data
}
[Fact]
- public async void Should_Return_UnsetValue_For_String_Not_Convertible_To_Boolean()
+ public async void Should_Return_BindingNotification_For_String_Not_Convertible_To_Boolean()
{
var data = new { Foo = "foo" };
var target = new ExpressionObserver(data, "!Foo");
var result = await target.Take(1);
- Assert.Equal(AvaloniaProperty.UnsetValue, result);
+ Assert.Equal(
+ new BindingNotification(
+ new InvalidCastException($"Unable to convert 'foo' to bool."),
+ BindingErrorType.Error),
+ result);
}
[Fact]
- public async void Should_Return_Empty_For_Value_Not_Convertible_To_Boolean()
+ public async void Should_Return_BindingNotification_For_Value_Not_Convertible_To_Boolean()
{
var data = new { Foo = new object() };
var target = new ExpressionObserver(data, "!Foo");
var result = await target.Take(1);
- Assert.Equal(AvaloniaProperty.UnsetValue, result);
+ Assert.Equal(
+ new BindingNotification(
+ new InvalidCastException($"Unable to convert 'System.Object' to bool."),
+ BindingErrorType.Error),
+ result);
}
[Fact]
diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs
index c5bb2886b5..3263aaace2 100644
--- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs
+++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Reactive.Subjects;
+using Avalonia.Data;
using Avalonia.Markup.Data;
using Avalonia.UnitTests;
using Xunit;
@@ -27,7 +28,7 @@ namespace Avalonia.Markup.UnitTests.Data
source.OnNext("bar");
sync.ExecutePostedCallbacks();
- Assert.Equal(new[] { AvaloniaProperty.UnsetValue, "foo", "bar" }, result);
+ Assert.Equal(new[] { "foo", "bar" }, result);
}
}
@@ -44,10 +45,50 @@ namespace Avalonia.Markup.UnitTests.Data
data.Next.OnNext(new Class2("foo"));
sync.ExecutePostedCallbacks();
- Assert.Equal(new[] { AvaloniaProperty.UnsetValue, "foo" }, result);
+ Assert.Equal(new[] { "foo" }, result);
sub.Dispose();
- Assert.Equal(0, data.SubscriptionCount);
+ Assert.Equal(0, data.PropertyChangedSubscriptionCount);
+ }
+ }
+
+ [Fact]
+ public void Should_Get_Simple_Observable_Value_With_DataValidation_Enabled()
+ {
+ using (var sync = UnitTestSynchronizationContext.Begin())
+ {
+ var source = new BehaviorSubject("foo");
+ var data = new { Foo = source };
+ var target = new ExpressionObserver(data, "Foo", true);
+ var result = new List();
+
+ var sub = target.Subscribe(x => result.Add(x));
+ source.OnNext("bar");
+ sync.ExecutePostedCallbacks();
+
+ // What does it mean to have data validation on an observable? Without a use-case
+ // it's hard to know what to do here so for the moment the value is returned.
+ Assert.Equal(new[] { "foo", "bar" }, result);
+ }
+ }
+
+ [Fact]
+ public void Should_Get_Property_Value_From_Observable_With_DataValidation_Enabled()
+ {
+ using (var sync = UnitTestSynchronizationContext.Begin())
+ {
+ var data = new Class1();
+ var target = new ExpressionObserver(data, "Next.Foo", true);
+ var result = new List();
+
+ var sub = target.Subscribe(x => result.Add(x));
+ data.Next.OnNext(new Class2("foo"));
+ sync.ExecutePostedCallbacks();
+
+ Assert.Equal(new[] { new BindingNotification("foo") }, result);
+
+ sub.Dispose();
+ Assert.Equal(0, data.PropertyChangedSubscriptionCount);
}
}
diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs
index 643b8fccab..aa9ee7d58b 100644
--- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs
+++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs
@@ -32,6 +32,8 @@ namespace Avalonia.Markup.UnitTests.Data
var data = new { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo");
+ target.Subscribe(_ => { });
+
Assert.Equal(typeof(string), target.ResultType);
}
@@ -55,6 +57,46 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.Equal("foo", result);
}
+ [Fact]
+ public async void Should_Return_UnsetValue_For_Root_Null()
+ {
+ var data = new Class3 { Foo = "foo" };
+ var target = new ExpressionObserver(default(object), "Foo");
+ var result = await target.Take(1);
+
+ Assert.Equal(AvaloniaProperty.UnsetValue, result);
+ }
+
+ [Fact]
+ public async void Should_Return_UnsetValue_For_Root_UnsetValue()
+ {
+ var data = new Class3 { Foo = "foo" };
+ var target = new ExpressionObserver(AvaloniaProperty.UnsetValue, "Foo");
+ var result = await target.Take(1);
+
+ Assert.Equal(AvaloniaProperty.UnsetValue, result);
+ }
+
+ [Fact]
+ public async void Should_Return_UnsetValue_For_Observable_Root_Null()
+ {
+ var data = new Class3 { Foo = "foo" };
+ var target = new ExpressionObserver(Observable.Return(default(object)), "Foo");
+ var result = await target.Take(1);
+
+ Assert.Equal(AvaloniaProperty.UnsetValue, result);
+ }
+
+ [Fact]
+ public async void Should_Return_UnsetValue_For_Observable_Root_UnsetValue()
+ {
+ var data = new Class3 { Foo = "foo" };
+ var target = new ExpressionObserver(Observable.Return(AvaloniaProperty.UnsetValue), "Foo");
+ var result = await target.Take(1);
+
+ Assert.Equal(AvaloniaProperty.UnsetValue, result);
+ }
+
[Fact]
public async void Should_Get_Simple_Property_Chain()
{
@@ -71,21 +113,44 @@ namespace Avalonia.Markup.UnitTests.Data
var data = new { Foo = new { Bar = new { Baz = "baz" } } };
var target = new ExpressionObserver(data, "Foo.Bar.Baz");
+ target.Subscribe(_ => { });
+
Assert.Equal(typeof(string), target.ResultType);
}
[Fact]
- public async void Should_Return_BindingError_For_Broken_Chain()
+ public async void Should_Return_BindingNotification_Error_For_Broken_Chain()
{
var data = new { Foo = new { Bar = 1 } };
var target = new ExpressionObserver(data, "Foo.Bar.Baz");
var result = await target.Take(1);
- Assert.IsType(result);
+ Assert.IsType(result);
- var error = result as BindingError;
- Assert.IsType(error.Exception);
- Assert.Equal("Could not find CLR property 'Baz' on '1'", error.Exception.Message);
+ Assert.Equal(
+ new BindingNotification(
+ new MissingMemberException("Could not find CLR property 'Baz' on '1'"), BindingErrorType.Error),
+ result);
+ }
+
+ [Fact]
+ public void Should_Return_BindingNotification_Error_For_Chain_With_Null_Value()
+ {
+ var data = new { Foo = default(object) };
+ var target = new ExpressionObserver(data, "Foo.Bar.Baz");
+ var result = new List();
+
+ target.Subscribe(x => result.Add(x));
+
+ Assert.Equal(
+ new[]
+ {
+ new BindingNotification(
+ new MarkupBindingChainNullException("Foo.Bar.Baz", "Foo"),
+ BindingErrorType.Error,
+ AvaloniaProperty.UnsetValue),
+ },
+ result);
}
[Fact]
@@ -111,7 +176,7 @@ namespace Avalonia.Markup.UnitTests.Data
sub.Dispose();
- Assert.Equal(0, data.SubscriptionCount);
+ Assert.Equal(0, data.PropertyChangedSubscriptionCount);
}
[Fact]
@@ -139,7 +204,7 @@ namespace Avalonia.Markup.UnitTests.Data
sub.Dispose();
- Assert.Equal(0, data.SubscriptionCount);
+ Assert.Equal(0, data.PropertyChangedSubscriptionCount);
}
[Fact]
@@ -151,13 +216,14 @@ namespace Avalonia.Markup.UnitTests.Data
var sub = target.Subscribe(x => result.Add(x));
((Class2)data.Next).Bar = "baz";
+ ((Class2)data.Next).Bar = null;
- Assert.Equal(new[] { "bar", "baz" }, result);
+ Assert.Equal(new[] { "bar", "baz", null }, result);
sub.Dispose();
- Assert.Equal(0, data.SubscriptionCount);
- Assert.Equal(0, data.Next.SubscriptionCount);
+ Assert.Equal(0, data.PropertyChangedSubscriptionCount);
+ Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount);
}
[Fact]
@@ -170,39 +236,60 @@ namespace Avalonia.Markup.UnitTests.Data
var sub = target.Subscribe(x => result.Add(x));
var old = data.Next;
data.Next = new Class2 { Bar = "baz" };
+ data.Next = new Class2 { Bar = null };
- Assert.Equal(new[] { "bar", "baz" }, result);
+ Assert.Equal(new[] { "bar", "baz", null }, result);
sub.Dispose();
- Assert.Equal(0, data.SubscriptionCount);
- Assert.Equal(0, data.Next.SubscriptionCount);
- Assert.Equal(0, old.SubscriptionCount);
+ Assert.Equal(0, data.PropertyChangedSubscriptionCount);
+ Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount);
+ Assert.Equal(0, old.PropertyChangedSubscriptionCount);
}
[Fact]
public void Should_Track_Property_Chain_Breaking_With_Null_Then_Mending()
{
- var data = new Class1 { Next = new Class2 { Bar = "bar" } };
- var target = new ExpressionObserver(data, "Next.Bar");
+ var data = new Class1
+ {
+ Next = new Class2
+ {
+ Next = new Class2
+ {
+ Bar = "bar"
+ }
+ }
+ };
+
+ var target = new ExpressionObserver(data, "Next.Next.Bar");
var result = new List();
var sub = target.Subscribe(x => result.Add(x));
var old = data.Next;
- data.Next = null;
data.Next = new Class2 { Bar = "baz" };
+ data.Next = old;
- Assert.Equal(new[] { "bar", AvaloniaProperty.UnsetValue, "baz" }, result);
+ Assert.Equal(
+ new object[]
+ {
+ "bar",
+ new BindingNotification(
+ new MarkupBindingChainNullException("Next.Next.Bar", "Next.Next"),
+ BindingErrorType.Error,
+ AvaloniaProperty.UnsetValue),
+ "bar"
+ },
+ result);
sub.Dispose();
- Assert.Equal(0, data.SubscriptionCount);
- Assert.Equal(0, data.Next.SubscriptionCount);
- Assert.Equal(0, old.SubscriptionCount);
+ Assert.Equal(0, data.PropertyChangedSubscriptionCount);
+ Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount);
+ Assert.Equal(0, old.PropertyChangedSubscriptionCount);
}
[Fact]
- public void Should_Track_Property_Chain_Breaking_With_Object_Then_Mending()
+ public void Should_Track_Property_Chain_Breaking_With_Missing_Member_Then_Mending()
{
var data = new Class1 { Next = new Class2 { Bar = "bar" } };
var target = new ExpressionObserver(data, "Next.Bar");
@@ -214,17 +301,23 @@ namespace Avalonia.Markup.UnitTests.Data
data.Next = breaking;
data.Next = new Class2 { Bar = "baz" };
- Assert.Equal(3, result.Count);
- Assert.Equal("bar", result[0]);
- Assert.IsType(result[1]);
- Assert.Equal("baz", result[2]);
+ Assert.Equal(
+ new object[]
+ {
+ "bar",
+ new BindingNotification(
+ new MissingMemberException("Could not find CLR property 'Bar' on 'Avalonia.Markup.UnitTests.Data.ExpressionObserverTests_Property+WithoutBar'"),
+ BindingErrorType.Error),
+ "baz",
+ },
+ result);
sub.Dispose();
- Assert.Equal(0, data.SubscriptionCount);
- Assert.Equal(0, data.Next.SubscriptionCount);
- Assert.Equal(0, breaking.SubscriptionCount);
- Assert.Equal(0, old.SubscriptionCount);
+ Assert.Equal(0, data.PropertyChangedSubscriptionCount);
+ Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount);
+ Assert.Equal(0, breaking.PropertyChangedSubscriptionCount);
+ Assert.Equal(0, old.PropertyChangedSubscriptionCount);
}
[Fact]
@@ -258,17 +351,59 @@ namespace Avalonia.Markup.UnitTests.Data
scheduler.Start();
}
- Assert.Equal(new[] { AvaloniaProperty.UnsetValue, "foo", "bar" }, result);
+ Assert.Equal(new[] { "foo", "bar" }, result);
Assert.All(source.Subscriptions, x => Assert.NotEqual(Subscription.Infinite, x.Unsubscribe));
}
+ [Fact]
+ public void Subscribing_Multiple_Times_Should_Return_Values_To_All()
+ {
+ var data = new Class1 { Foo = "foo" };
+ var target = new ExpressionObserver(data, "Foo");
+ var result1 = new List();
+ var result2 = new List();
+ var result3 = new List();
+
+ target.Subscribe(x => result1.Add(x));
+ target.Subscribe(x => result2.Add(x));
+
+ data.Foo = "bar";
+
+ target.Subscribe(x => result3.Add(x));
+
+ Assert.Equal(new[] { "foo", "bar" }, result1);
+ Assert.Equal(new[] { "foo", "bar" }, result2);
+ Assert.Equal(new[] { "bar" }, result3);
+ }
+
+ [Fact]
+ public void Subscribing_Multiple_Times_Should_Only_Add_PropertyChanged_Handlers_Once()
+ {
+ var data = new Class1 { Foo = "foo" };
+ var target = new ExpressionObserver(data, "Foo");
+
+ var sub1 = target.Subscribe(x => { });
+ var sub2 = target.Subscribe(x => { });
+
+ Assert.Equal(1, data.PropertyChangedSubscriptionCount);
+
+ sub1.Dispose();
+ sub2.Dispose();
+
+ Assert.Equal(0, data.PropertyChangedSubscriptionCount);
+ }
+
[Fact]
public void SetValue_Should_Set_Simple_Property_Value()
{
var data = new Class1 { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo");
- Assert.True(target.SetValue("bar"));
+ using (target.Subscribe(_ => { }))
+ {
+ Assert.True(target.SetValue("bar"));
+ }
+
Assert.Equal("bar", data.Foo);
}
@@ -278,7 +413,11 @@ namespace Avalonia.Markup.UnitTests.Data
var data = new Class1 { Next = new Class2 { Bar = "bar" } };
var target = new ExpressionObserver(data, "Next.Bar");
- Assert.True(target.SetValue("baz"));
+ using (target.Subscribe(_ => { }))
+ {
+ Assert.True(target.SetValue("baz"));
+ }
+
Assert.Equal("baz", ((Class2)data.Next).Bar);
}
@@ -288,25 +427,48 @@ namespace Avalonia.Markup.UnitTests.Data
var data = new Class1 { Next = new WithoutBar()};
var target = new ExpressionObserver(data, "Next.Bar");
- Assert.False(target.SetValue("baz"));
+ using (target.Subscribe(_ => { }))
+ {
+ Assert.False(target.SetValue("baz"));
+ }
}
[Fact]
- public void SetValue_Should_Return_False_For_Missing_Object()
+ public void SetValue_Should_Notify_New_Value_With_Inpc()
{
var data = new Class1();
- var target = new ExpressionObserver(data, "Next.Bar");
+ var target = new ExpressionObserver(data, "Foo");
+ var result = new List();
- Assert.False(target.SetValue("baz"));
+ target.Subscribe(x => result.Add(x));
+ target.SetValue("bar");
+
+ Assert.Equal(new[] { null, "bar" }, result);
}
[Fact]
- public async void Should_Handle_Null_Root()
+ public void SetValue_Should_Notify_New_Value_Without_Inpc()
{
- var target = new ExpressionObserver((object)null, "Foo");
- var result = await target.Take(1);
+ var data = new Class1();
+ var target = new ExpressionObserver(data, "Bar");
+ var result = new List();
- Assert.Equal(AvaloniaProperty.UnsetValue, result);
+ target.Subscribe(x => result.Add(x));
+ target.SetValue("bar");
+
+ Assert.Equal(new[] { null, "bar" }, result);
+ }
+
+ [Fact]
+ public void SetValue_Should_Return_False_For_Missing_Object()
+ {
+ var data = new Class1();
+ var target = new ExpressionObserver(data, "Next.Bar");
+
+ using (target.Subscribe(_ => { }))
+ {
+ Assert.False(target.SetValue("baz"));
+ }
}
[Fact]
@@ -325,10 +487,17 @@ namespace Avalonia.Markup.UnitTests.Data
root = null;
update.OnNext(Unit.Default);
- Assert.Equal(new[] { "foo", "bar", AvaloniaProperty.UnsetValue }, result);
-
- Assert.Equal(0, first.SubscriptionCount);
- Assert.Equal(0, second.SubscriptionCount);
+ Assert.Equal(
+ new object[]
+ {
+ "foo",
+ "bar",
+ AvaloniaProperty.UnsetValue,
+ },
+ result);
+
+ Assert.Equal(0, first.PropertyChangedSubscriptionCount);
+ Assert.Equal(0, second.PropertyChangedSubscriptionCount);
}
[Fact]
@@ -351,7 +520,7 @@ namespace Avalonia.Markup.UnitTests.Data
private interface INext
{
- int SubscriptionCount { get; }
+ int PropertyChangedSubscriptionCount { get; }
}
private class Class1 : NotifyingBase
@@ -390,6 +559,7 @@ namespace Avalonia.Markup.UnitTests.Data
private class Class2 : NotifyingBase, INext
{
private string _bar;
+ private INext _next;
public string Bar
{
@@ -400,6 +570,16 @@ namespace Avalonia.Markup.UnitTests.Data
RaisePropertyChanged(nameof(Bar));
}
}
+
+ public INext Next
+ {
+ get { return _next; }
+ set
+ {
+ _next = value;
+ RaisePropertyChanged(nameof(Next));
+ }
+ }
}
private class Class3 : Class1
diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_SetValue.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_SetValue.cs
index 4dabd34460..3238435841 100644
--- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_SetValue.cs
+++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_SetValue.cs
@@ -18,7 +18,10 @@ namespace Avalonia.Markup.UnitTests.Data
var data = new { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo");
- target.SetValue("bar");
+ using (target.Subscribe(_ => { }))
+ {
+ target.SetValue("bar");
+ }
Assert.Equal("foo", data.Foo);
}
@@ -29,7 +32,10 @@ namespace Avalonia.Markup.UnitTests.Data
var data = new Class1 { Foo = new Class2 { Bar = "bar" } };
var target = new ExpressionObserver(data, "Foo.Bar");
- target.SetValue("foo");
+ using (target.Subscribe(_ => { }))
+ {
+ target.SetValue("foo");
+ }
Assert.Equal("foo", data.Foo.Bar);
}
diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Task.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Task.cs
index 3d4c0b1b43..3dcd8a4fbc 100644
--- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Task.cs
+++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Task.cs
@@ -4,8 +4,8 @@
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
-using System.Threading;
using System.Threading.Tasks;
+using Avalonia.Data;
using Avalonia.Markup.Data;
using Avalonia.UnitTests;
using Xunit;
@@ -28,7 +28,7 @@ namespace Avalonia.Markup.UnitTests.Data
tcs.SetResult("foo");
sync.ExecutePostedCallbacks();
- Assert.Equal(new object[] { AvaloniaProperty.UnsetValue, "foo" }, result.ToArray());
+ Assert.Equal(new[] { "foo" }, result);
}
}
@@ -43,7 +43,7 @@ namespace Avalonia.Markup.UnitTests.Data
var sub = target.Subscribe(x => result.Add(x));
- Assert.Equal(new object[] { "foo" }, result.ToArray());
+ Assert.Equal(new[] { "foo" }, result);
}
}
@@ -61,10 +61,84 @@ namespace Avalonia.Markup.UnitTests.Data
tcs.SetResult(new Class2("foo"));
sync.ExecutePostedCallbacks();
- Assert.Equal(new object[] { AvaloniaProperty.UnsetValue, "foo" }, result.ToArray());
+ Assert.Equal(new[] { "foo" }, result);
}
}
+ [Fact]
+ public void Should_Return_BindingNotification_Error_On_Task_Exception()
+ {
+ using (var sync = UnitTestSynchronizationContext.Begin())
+ {
+ var tcs = new TaskCompletionSource();
+ var data = new { Foo = tcs.Task };
+ var target = new ExpressionObserver(data, "Foo");
+ var result = new List();
+
+ var sub = target.Subscribe(x => result.Add(x));
+ tcs.SetException(new NotSupportedException());
+ sync.ExecutePostedCallbacks();
+
+ Assert.Equal(
+ new[]
+ {
+ new BindingNotification(
+ new AggregateException(new NotSupportedException()),
+ BindingErrorType.Error)
+ },
+ result);
+ }
+ }
+
+ [Fact]
+ public void Should_Return_BindingNotification_Error_For_Faulted_Task()
+ {
+ using (var sync = UnitTestSynchronizationContext.Begin())
+ {
+ var data = new { Foo = TaskFromException(new NotSupportedException()) };
+ var target = new ExpressionObserver(data, "Foo");
+ var result = new List();
+
+ var sub = target.Subscribe(x => result.Add(x));
+
+ Assert.Equal(
+ new[]
+ {
+ new BindingNotification(
+ new AggregateException(new NotSupportedException()),
+ BindingErrorType.Error)
+ },
+ result);
+ }
+ }
+
+ [Fact]
+ public void Should_Get_Simple_Task_Value_With_Data_DataValidation_Enabled()
+ {
+ using (var sync = UnitTestSynchronizationContext.Begin())
+ {
+ var tcs = new TaskCompletionSource();
+ var data = new { Foo = tcs.Task };
+ var target = new ExpressionObserver(data, "Foo", true);
+ var result = new List();
+
+ var sub = target.Subscribe(x => result.Add(x));
+ tcs.SetResult("foo");
+ sync.ExecutePostedCallbacks();
+
+ // What does it mean to have data validation on a Task? Without a use-case it's
+ // hard to know what to do here so for the moment the value is returned.
+ Assert.Equal(new [] { "foo" }, result);
+ }
+ }
+
+ private Task TaskFromException(Exception e)
+ {
+ var tcs = new TaskCompletionSource();
+ tcs.SetException(e);
+ return tcs.Task;
+ }
+
private class Class1 : NotifyingBase
{
public Class1(Task next)
diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Validation.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Validation.cs
deleted file mode 100644
index 59c8965cfb..0000000000
--- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Validation.cs
+++ /dev/null
@@ -1,129 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Linq;
-using System.Reactive.Linq;
-using System.Runtime.CompilerServices;
-using Avalonia.Data;
-using Avalonia.Markup.Data;
-using Xunit;
-
-namespace Avalonia.Markup.UnitTests.Data
-{
- public class ExpressionObserverTests_Validation
- {
- [Fact]
- public void Exception_Validation_Sends_ValidationUpdate()
- {
- var data = new ExceptionTest { MustBePositive = 5 };
- var observer = new ExpressionObserver(data, nameof(data.MustBePositive), false);
- var validationMessageFound = false;
- observer.Where(o => o is IValidationStatus).Subscribe(_ => validationMessageFound = true);
- observer.SetValue(-5);
- Assert.True(validationMessageFound);
- }
-
- [Fact]
- public void Disabled_Indei_Validation_Does_Not_Subscribe()
- {
- var data = new IndeiTest { MustBePositive = 5 };
- var observer = new ExpressionObserver(data, nameof(data.MustBePositive), false);
-
- observer.Subscribe(_ => { });
-
- Assert.Equal(0, data.SubscriptionCount);
- }
-
- [Fact]
- public void Enabled_Indei_Validation_Subscribes()
- {
- var data = new IndeiTest { MustBePositive = 5 };
- var observer = new ExpressionObserver(data, nameof(data.MustBePositive), true);
- var sub = observer.Subscribe(_ => { });
-
- Assert.Equal(1, data.SubscriptionCount);
- sub.Dispose();
- Assert.Equal(0, data.SubscriptionCount);
- }
-
- public class ExceptionTest : INotifyPropertyChanged
- {
- private int _mustBePositive;
-
- public int MustBePositive
- {
- get { return _mustBePositive; }
- set
- {
- if (value <= 0)
- {
- throw new ArgumentOutOfRangeException(nameof(value));
- }
- _mustBePositive = value;
- }
- }
-
- public event PropertyChangedEventHandler PropertyChanged;
-
- private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
- {
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
- }
- }
-
- private class IndeiTest : INotifyDataErrorInfo
- {
- private int _mustBePositive;
- private Dictionary> _errors = new Dictionary>();
- private EventHandler _errorsChanged;
-
- public int MustBePositive
- {
- get { return _mustBePositive; }
- set
- {
- if (value >= 0)
- {
- _mustBePositive = value;
- _errors.Remove(nameof(MustBePositive));
- _errorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(MustBePositive)));
- }
- else
- {
- _errors[nameof(MustBePositive)] = new[] { "Must be positive" };
- _errorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(MustBePositive)));
- }
- }
- }
-
- public bool HasErrors => _mustBePositive >= 0;
-
- public int SubscriptionCount { get; private set; }
-
- public event EventHandler ErrorsChanged
- {
- add
- {
- _errorsChanged += value;
- ++SubscriptionCount;
- }
- remove
- {
- _errorsChanged -= value;
- --SubscriptionCount;
- }
- }
-
- public IEnumerable GetErrors(string propertyName)
- {
- IList result;
- _errors.TryGetValue(propertyName, out result);
- return result;
- }
- }
- }
-}
diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionSubjectTests.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionSubjectTests.cs
deleted file mode 100644
index 0b6a507274..0000000000
--- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionSubjectTests.cs
+++ /dev/null
@@ -1,198 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.ComponentModel;
-using System.Globalization;
-using System.Reactive.Linq;
-using Moq;
-using Avalonia.Data;
-using Avalonia.Markup.Data;
-using Xunit;
-using System.Threading;
-
-namespace Avalonia.Markup.UnitTests.Data
-{
- public class ExpressionSubjectTests
- {
- [Fact]
- public async void Should_Get_Simple_Property_Value()
- {
- var data = new Class1 { StringValue = "foo" };
- var target = new ExpressionSubject(new ExpressionObserver(data, "StringValue"), typeof(string));
- var result = await target.Take(1);
-
- Assert.Equal("foo", result);
- }
-
- [Fact]
- public void Should_Set_Simple_Property_Value()
- {
- var data = new Class1 { StringValue = "foo" };
- var target = new ExpressionSubject(new ExpressionObserver(data, "StringValue"), typeof(string));
-
- target.OnNext("bar");
-
- Assert.Equal("bar", data.StringValue);
- }
-
- [Fact]
- public async void Should_Convert_Get_String_To_Double()
- {
- Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
-
- var data = new Class1 { StringValue = "5.6" };
- var target = new ExpressionSubject(new ExpressionObserver(data, "StringValue"), typeof(double));
- var result = await target.Take(1);
-
- Assert.Equal(5.6, result);
- }
-
- [Fact]
- public async void Getting_Invalid_Double_String_Should_Return_BindingError()
- {
- var data = new Class1 { StringValue = "foo" };
- var target = new ExpressionSubject(new ExpressionObserver(data, "StringValue"), typeof(double));
- var result = await target.Take(1);
-
- Assert.IsType(result);
- }
-
- [Fact]
- public async void Should_Coerce_Get_Null_Double_String_To_UnsetValue()
- {
- var data = new Class1 { StringValue = null };
- var target = new ExpressionSubject(new ExpressionObserver(data, "StringValue"), typeof(double));
- var result = await target.Take(1);
-
- Assert.Equal(AvaloniaProperty.UnsetValue, result);
- }
-
- [Fact]
- public void Should_Convert_Set_String_To_Double()
- {
- Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
-
- var data = new Class1 { StringValue = (5.6).ToString() };
- var target = new ExpressionSubject(new ExpressionObserver(data, "StringValue"), typeof(double));
-
- target.OnNext(6.7);
-
- Assert.Equal((6.7).ToString(), data.StringValue);
- }
-
- [Fact]
- public async void Should_Convert_Get_Double_To_String()
- {
- Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
-
- var data = new Class1 { DoubleValue = 5.6 };
- var target = new ExpressionSubject(new ExpressionObserver(data, "DoubleValue"), typeof(string));
- var result = await target.Take(1);
-
- Assert.Equal((5.6).ToString(), result);
- }
-
- [Fact]
- public void Should_Convert_Set_Double_To_String()
- {
- Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
-
- var data = new Class1 { DoubleValue = 5.6 };
- var target = new ExpressionSubject(new ExpressionObserver(data, "DoubleValue"), typeof(string));
-
- target.OnNext("6.7");
-
- Assert.Equal(6.7, data.DoubleValue);
- }
-
- [Fact]
- public void Setting_Invalid_Double_String_Should_Not_Change_Target()
- {
- var data = new Class1 { DoubleValue = 5.6 };
- var target = new ExpressionSubject(new ExpressionObserver(data, "DoubleValue"), typeof(string));
-
- target.OnNext("foo");
-
- Assert.Equal(5.6, data.DoubleValue);
- }
-
- [Fact]
- public void Setting_Invalid_Double_String_Should_Use_FallbackValue()
- {
- var data = new Class1 { DoubleValue = 5.6 };
- var target = new ExpressionSubject(
- new ExpressionObserver(data, "DoubleValue"),
- typeof(string),
- "9.8",
- DefaultValueConverter.Instance);
-
- target.OnNext("foo");
-
- Assert.Equal(9.8, data.DoubleValue);
- }
-
- [Fact]
- public void Should_Coerce_Setting_Null_Double_To_Default_Value()
- {
- var data = new Class1 { DoubleValue = 5.6 };
- var target = new ExpressionSubject(new ExpressionObserver(data, "DoubleValue"), typeof(string));
-
- target.OnNext(null);
-
- Assert.Equal(0, data.DoubleValue);
- }
-
- [Fact]
- public void Should_Coerce_Setting_UnsetValue_Double_To_Default_Value()
- {
- var data = new Class1 { DoubleValue = 5.6 };
- var target = new ExpressionSubject(new ExpressionObserver(data, "DoubleValue"), typeof(string));
-
- target.OnNext(AvaloniaProperty.UnsetValue);
-
- Assert.Equal(0, data.DoubleValue);
- }
-
- [Fact]
- public void Should_Pass_ConverterParameter_To_Convert()
- {
- var data = new Class1 { DoubleValue = 5.6 };
- var converter = new Mock();
- var target = new ExpressionSubject(
- new ExpressionObserver(data, "DoubleValue"),
- typeof(string),
- converter.Object,
- converterParameter: "foo");
-
- target.Subscribe(_ => { });
-
- converter.Verify(x => x.Convert(5.6, typeof(string), "foo", CultureInfo.CurrentUICulture));
- }
-
- [Fact]
- public void Should_Pass_ConverterParameter_To_ConvertBack()
- {
- var data = new Class1 { DoubleValue = 5.6 };
- var converter = new Mock();
- var target = new ExpressionSubject(
- new ExpressionObserver(data, "DoubleValue"),
- typeof(string),
- converter.Object,
- converterParameter: "foo");
-
- target.OnNext("bar");
-
- converter.Verify(x => x.ConvertBack("bar", typeof(double), "foo", CultureInfo.CurrentUICulture));
- }
-
- private class Class1 : INotifyPropertyChanged
- {
- public event PropertyChangedEventHandler PropertyChanged;
-
- public string StringValue { get; set; }
-
- public double DoubleValue { get; set; }
- }
- }
-}
diff --git a/tests/Avalonia.Markup.UnitTests/Data/IndeiBase.cs b/tests/Avalonia.Markup.UnitTests/Data/IndeiBase.cs
new file mode 100644
index 0000000000..bd0ab71626
--- /dev/null
+++ b/tests/Avalonia.Markup.UnitTests/Data/IndeiBase.cs
@@ -0,0 +1,32 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using Avalonia.UnitTests;
+
+namespace Avalonia.Markup.UnitTests.Data
+{
+ internal abstract class IndeiBase : NotifyingBase, INotifyDataErrorInfo
+ {
+ private EventHandler _errorsChanged;
+
+ public abstract bool HasErrors { get; }
+ public int ErrorsChangedSubscriptionCount { get; private set; }
+
+ public event EventHandler ErrorsChanged
+ {
+ add { _errorsChanged += value; ++ErrorsChangedSubscriptionCount; }
+ remove { _errorsChanged -= value; --ErrorsChangedSubscriptionCount; }
+ }
+
+ public abstract IEnumerable GetErrors(string propertyName);
+
+ protected void RaiseErrorsChanged([CallerMemberName] string propertyName = "")
+ {
+ _errorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
+ }
+ }
+}
diff --git a/tests/Avalonia.Markup.UnitTests/Data/IndeiValidatorTests.cs b/tests/Avalonia.Markup.UnitTests/Data/IndeiValidatorTests.cs
deleted file mode 100644
index 20bf164360..0000000000
--- a/tests/Avalonia.Markup.UnitTests/Data/IndeiValidatorTests.cs
+++ /dev/null
@@ -1,113 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.Collections;
-using System.ComponentModel;
-using System.Runtime.CompilerServices;
-using Avalonia.Data;
-using Avalonia.Markup.Data.Plugins;
-using Xunit;
-
-namespace Avalonia.Markup.UnitTests.Data
-{
- public class IndeiValidatorTests
- {
- public class Data : INotifyPropertyChanged, INotifyDataErrorInfo
- {
- private int nonValidated;
-
- public int NonValidated
- {
- get { return nonValidated; }
- set { nonValidated = value; NotifyPropertyChanged(); }
- }
-
- private int mustBePositive;
-
- public int MustBePositive
- {
- get { return mustBePositive; }
- set
- {
- mustBePositive = value;
- NotifyErrorsChanged();
- }
- }
-
- public bool HasErrors
- {
- get
- {
- return MustBePositive > 0;
- }
- }
-
- public event PropertyChangedEventHandler PropertyChanged;
- public event EventHandler ErrorsChanged;
-
- private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
- {
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
- }
-
- private void NotifyErrorsChanged([CallerMemberName] string propertyName = "")
- {
- ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
- }
-
- public IEnumerable GetErrors(string propertyName)
- {
- if (propertyName == nameof(MustBePositive) && MustBePositive <= 0)
- {
- yield return $"{nameof(MustBePositive)} must be positive";
- }
- }
- }
-
- [Fact]
- public void Setting_Non_Validating_Does_Not_Trigger_Validation()
- {
- var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
- var validatorPlugin = new IndeiValidationPlugin();
- var data = new Data();
- var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.NonValidated), _ => { });
- IValidationStatus status = null;
- var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.NonValidated), accessor, s => status = s);
-
- validator.SetValue(5, BindingPriority.LocalValue);
-
- Assert.Null(status);
- }
-
- [Fact]
- public void Setting_Validating_Property_To_Valid_Value_Returns_Successful_ValidationStatus()
- {
- var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
- var validatorPlugin = new IndeiValidationPlugin();
- var data = new Data();
- var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), _ => { });
- IValidationStatus status = null;
- var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor, s => status = s);
-
- validator.SetValue(5, BindingPriority.LocalValue);
-
- Assert.True(status.IsValid);
- }
-
- [Fact]
- public void Setting_Validating_Property_To_Invalid_Value_Returns_Failed_ValidationStatus()
- {
- var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
- var validatorPlugin = new IndeiValidationPlugin();
- var data = new Data();
- var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), _ => { });
- IValidationStatus status = null;
- var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor, s => status = s);
-
- validator.SetValue(-5, BindingPriority.LocalValue);
-
- Assert.False(status.IsValid);
- }
- }
-}
diff --git a/tests/Avalonia.Markup.UnitTests/Data/Plugins/DataAnnotationsValidationPluginTests.cs b/tests/Avalonia.Markup.UnitTests/Data/Plugins/DataAnnotationsValidationPluginTests.cs
new file mode 100644
index 0000000000..b873971e7f
--- /dev/null
+++ b/tests/Avalonia.Markup.UnitTests/Data/Plugins/DataAnnotationsValidationPluginTests.cs
@@ -0,0 +1,111 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using Avalonia.Data;
+using Avalonia.Markup.Data.Plugins;
+using Xunit;
+
+namespace Avalonia.Markup.UnitTests.Data.Plugins
+{
+ public class DataAnnotationsValidationPluginTests
+ {
+ [Fact]
+ public void Should_Match_Property_With_ValidatorAttribute()
+ {
+ var target = new DataAnnotationsValidationPlugin();
+ var data = new Data();
+
+ Assert.True(target.Match(new WeakReference(data), nameof(Data.Between5And10)));
+ }
+
+ [Fact]
+ public void Should_Match_Property_With_Multiple_ValidatorAttributes()
+ {
+ var target = new DataAnnotationsValidationPlugin();
+ var data = new Data();
+
+ Assert.True(target.Match(new WeakReference(data), nameof(Data.PhoneNumber)));
+ }
+
+ [Fact]
+ public void Should_Not_Match_Property_Without_ValidatorAttribute()
+ {
+ var target = new DataAnnotationsValidationPlugin();
+ var data = new Data();
+
+ Assert.False(target.Match(new WeakReference(data), nameof(Data.Unvalidated)));
+ }
+
+ [Fact]
+ public void Produces_Range_BindingNotificationsx()
+ {
+ var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
+ var validatorPlugin = new DataAnnotationsValidationPlugin();
+ var data = new Data();
+ var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.Between5And10));
+ var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Between5And10), accessor);
+ var result = new List();
+
+ validator.Subscribe(x => result.Add(x));
+ validator.SetValue(3, BindingPriority.LocalValue);
+ validator.SetValue(7, BindingPriority.LocalValue);
+ validator.SetValue(11, BindingPriority.LocalValue);
+
+ Assert.Equal(new[]
+ {
+ new BindingNotification(5),
+ new BindingNotification(
+ new ValidationException("The field Between5And10 must be between 5 and 10."),
+ BindingErrorType.DataValidationError,
+ 3),
+ new BindingNotification(7),
+ new BindingNotification(
+ new ValidationException("The field Between5And10 must be between 5 and 10."),
+ BindingErrorType.DataValidationError,
+ 11),
+ }, result);
+ }
+
+ [Fact]
+ public void Produces_Aggregate_BindingNotificationsx()
+ {
+ var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
+ var validatorPlugin = new DataAnnotationsValidationPlugin();
+ var data = new Data();
+ var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.PhoneNumber));
+ var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.PhoneNumber), accessor);
+ var result = new List();
+
+ validator.Subscribe(x => result.Add(x));
+ validator.SetValue("123456", BindingPriority.LocalValue);
+ validator.SetValue("abcdefghijklm", BindingPriority.LocalValue);
+
+ Assert.Equal(new[]
+ {
+ new BindingNotification(null),
+ new BindingNotification("123456"),
+ new BindingNotification(
+ new AggregateException(
+ new ValidationException("The PhoneNumber field is not a valid phone number."),
+ new ValidationException("The field PhoneNumber must be a string or array type with a maximum length of '10'.")),
+ BindingErrorType.DataValidationError,
+ "abcdefghijklm"),
+ }, result);
+ }
+
+ private class Data
+ {
+ [Range(5, 10)]
+ public int Between5And10 { get; set; } = 5;
+
+ public int Unvalidated { get; set; }
+
+ [Phone]
+ [MaxLength(10)]
+ public string PhoneNumber { get; set; }
+ }
+ }
+}
diff --git a/tests/Avalonia.Markup.UnitTests/Data/Plugins/ExceptionValidationPluginTests.cs b/tests/Avalonia.Markup.UnitTests/Data/Plugins/ExceptionValidationPluginTests.cs
new file mode 100644
index 0000000000..4a34791008
--- /dev/null
+++ b/tests/Avalonia.Markup.UnitTests/Data/Plugins/ExceptionValidationPluginTests.cs
@@ -0,0 +1,63 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Reactive.Linq;
+using Avalonia.Data;
+using Avalonia.Markup.Data.Plugins;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Markup.UnitTests.Data.Plugins
+{
+ public class ExceptionValidationPluginTests
+ {
+ [Fact]
+ public void Produces_BindingNotifications()
+ {
+ var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
+ var validatorPlugin = new ExceptionValidationPlugin();
+ var data = new Data();
+ var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive));
+ var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor);
+ var result = new List();
+
+ validator.Subscribe(x => result.Add(x));
+ validator.SetValue(5, BindingPriority.LocalValue);
+ validator.SetValue(-2, BindingPriority.LocalValue);
+ validator.SetValue(6, BindingPriority.LocalValue);
+
+ Assert.Equal(new[]
+ {
+ new BindingNotification(0),
+ new BindingNotification(5),
+ new BindingNotification(new ArgumentOutOfRangeException("value"), BindingErrorType.DataValidationError),
+ new BindingNotification(6),
+ }, result);
+ }
+
+ public class Data : NotifyingBase
+ {
+ private int _mustBePositive;
+
+ public int MustBePositive
+ {
+ get { return _mustBePositive; }
+ set
+ {
+ if (value <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+
+ if (value != _mustBePositive)
+ {
+ _mustBePositive = value;
+ RaisePropertyChanged();
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/tests/Avalonia.Markup.UnitTests/Data/Plugins/IndeiValidationPluginTests.cs b/tests/Avalonia.Markup.UnitTests/Data/Plugins/IndeiValidationPluginTests.cs
new file mode 100644
index 0000000000..788bc25a34
--- /dev/null
+++ b/tests/Avalonia.Markup.UnitTests/Data/Plugins/IndeiValidationPluginTests.cs
@@ -0,0 +1,127 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Reactive.Linq;
+using Avalonia.Data;
+using Avalonia.Markup.Data.Plugins;
+using Xunit;
+
+namespace Avalonia.Markup.UnitTests.Data.Plugins
+{
+ public class IndeiValidationPluginTests
+ {
+ [Fact]
+ public void Produces_BindingNotifications()
+ {
+ var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
+ var validatorPlugin = new IndeiValidationPlugin();
+ var data = new Data { Maximum = 5 };
+ var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.Value));
+ var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Value), accessor);
+ var result = new List();
+
+ validator.Subscribe(x => result.Add(x));
+ validator.SetValue(5, BindingPriority.LocalValue);
+ validator.SetValue(6, BindingPriority.LocalValue);
+ data.Maximum = 10;
+ data.Maximum = 5;
+
+ Assert.Equal(new[]
+ {
+ new BindingNotification(0),
+ new BindingNotification(5),
+
+ // Value is first signalled without an error as validation hasn't been updated.
+ new BindingNotification(6),
+
+ // Then the ErrorsChanged event is fired.
+ new BindingNotification(new Exception("Must be less than Maximum"), BindingErrorType.DataValidationError, 6),
+
+ // Maximum is changed to 10 so value is now valid.
+ new BindingNotification(6),
+
+ // And Maximum is changed back to 5.
+ new BindingNotification(new Exception("Must be less than Maximum"), BindingErrorType.DataValidationError, 6),
+ }, result);
+ }
+
+ [Fact]
+ public void Subscribes_And_Unsubscribes()
+ {
+ var inpcAccessorPlugin = new InpcPropertyAccessorPlugin();
+ var validatorPlugin = new IndeiValidationPlugin();
+ var data = new Data { Maximum = 5 };
+ var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.Value));
+ var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Value), accessor);
+
+ Assert.Equal(0, data.ErrorsChangedSubscriptionCount);
+ var sub = validator.Subscribe(_ => { });
+ Assert.Equal(1, data.ErrorsChangedSubscriptionCount);
+ sub.Dispose();
+ Assert.Equal(0, data.ErrorsChangedSubscriptionCount);
+ }
+
+ internal class Data : IndeiBase
+ {
+ private int _value;
+ private int _maximum;
+ private string _error;
+
+ public override bool HasErrors => _error != null;
+
+ public int Value
+ {
+ get { return _value; }
+ set
+ {
+ _value = value;
+ RaisePropertyChanged();
+ UpdateError();
+ }
+ }
+
+ public int Maximum
+ {
+ get { return _maximum; }
+ set
+ {
+ _maximum = value;
+ UpdateError();
+ }
+ }
+
+ public override IEnumerable GetErrors(string propertyName)
+ {
+ if (propertyName == nameof(Value) && _error != null)
+ {
+ return new[] { _error };
+ }
+
+ return null;
+ }
+
+ private void UpdateError()
+ {
+ if (_value <= _maximum)
+ {
+ if (_error != null)
+ {
+ _error = null;
+ RaiseErrorsChanged(nameof(Value));
+ }
+ }
+ else
+ {
+ if (_error == null)
+ {
+ _error = "Must be less than Maximum";
+ RaiseErrorsChanged(nameof(Value));
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/tests/Avalonia.Markup.UnitTests/DefaultValueConverterTests.cs b/tests/Avalonia.Markup.UnitTests/DefaultValueConverterTests.cs
index 2e52dff087..fd28f2e900 100644
--- a/tests/Avalonia.Markup.UnitTests/DefaultValueConverterTests.cs
+++ b/tests/Avalonia.Markup.UnitTests/DefaultValueConverterTests.cs
@@ -115,7 +115,7 @@ namespace Avalonia.Markup.UnitTests
null,
CultureInfo.InvariantCulture);
- Assert.IsType(result);
+ Assert.IsType(result);
}
private enum TestEnum
diff --git a/tests/Avalonia.Markup.UnitTests/app.config b/tests/Avalonia.Markup.UnitTests/app.config
index fa66e8c206..654f911514 100644
--- a/tests/Avalonia.Markup.UnitTests/app.config
+++ b/tests/Avalonia.Markup.UnitTests/app.config
@@ -1,11 +1,11 @@
-
+
-
-
+
+
-
\ No newline at end of file
+
diff --git a/tests/Avalonia.Markup.UnitTests/packages.config b/tests/Avalonia.Markup.UnitTests/packages.config
index 34563ef392..d264c076fd 100644
--- a/tests/Avalonia.Markup.UnitTests/packages.config
+++ b/tests/Avalonia.Markup.UnitTests/packages.config
@@ -1,11 +1,14 @@
-
+
-
-
-
-
+
+
+
+
+
+
+
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj
index 0e2e11c300..54e61fc7d2 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj
@@ -93,9 +93,9 @@
+
-
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs
index 7d8528c5d7..210ad2ab0b 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs
@@ -3,6 +3,8 @@
using System;
using System.Collections.Generic;
+using System.Linq;
+using System.Reactive.Linq;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Markup.Data;
@@ -164,17 +166,23 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
[Fact]
public void DataContext_Binding_Should_Produce_Correct_Results()
{
+ var viewModel = new { Foo = "bar" };
var root = new Decorator
{
- DataContext = new { Foo = "bar" },
+ DataContext = viewModel,
};
var child = new Control();
- var dataContextBinding = new Binding("Foo");
var values = new List();
- child.GetObservable(Border.DataContextProperty).Subscribe(x => values.Add(x));
- child.Bind(ContentControl.DataContextProperty, dataContextBinding);
+ child.GetObservable(Control.DataContextProperty).Subscribe(x => values.Add(x));
+ child.Bind(Control.DataContextProperty, new Binding("Foo"));
+
+ // When binding to DataContext and the target isn't found, the binding should produce
+ // null rather than UnsetValue in order to not propagate incorrect DataContexts from
+ // parent controls while things are being set up. This logic is implemented in
+ // `Avalonia.Markup.Xaml.Binding.Initiate`.
+ Assert.True(child.IsSet(Control.DataContextProperty));
root.Child = child;
@@ -192,7 +200,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
var result = binding.Initiate(target, TextBox.TextProperty).Subject;
- Assert.IsType(((ExpressionSubject)result).Converter);
+ Assert.IsType(((BindingExpression)result).Converter);
}
[Fact]
@@ -208,7 +216,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
var result = binding.Initiate(target, TextBox.TextProperty).Subject;
- Assert.Same(converter.Object, ((ExpressionSubject)result).Converter);
+ Assert.Same(converter.Object, ((BindingExpression)result).Converter);
}
[Fact]
@@ -225,7 +233,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
var result = binding.Initiate(target, TextBox.TextProperty).Subject;
- Assert.Same("foo", ((ExpressionSubject)result).ConverterParameter);
+ Assert.Same("foo", ((BindingExpression)result).ConverterParameter);
}
[Fact]
@@ -267,7 +275,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
///
///
/// - Items is bound to DataContext first, followed by say SelectedIndex
- /// - When the ListBox is removed from the visual tree, DataContext becomes null (as it's
+ /// - When the ListBox is removed from the logical tree, DataContext becomes null (as it's
/// inherited)
/// - This changes Items to null, which changes SelectedIndex to null as there are no
/// longer any items
@@ -294,12 +302,12 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
// Bind Foo and Bar to the VM.
target.Bind(OldDataContextTest.FooProperty, fooBinding);
- target.Bind(OldDataContextTest.BarProperty, barBinding);
+ //target.Bind(OldDataContextTest.BarProperty, barBinding);
target.DataContext = vm;
// Make sure the control's Foo and Bar properties are read from the VM
Assert.Equal(1, target.GetValue(OldDataContextTest.FooProperty));
- Assert.Equal(2, target.GetValue(OldDataContextTest.BarProperty));
+ //Assert.Equal(2, target.GetValue(OldDataContextTest.BarProperty));
// Set DataContext to null.
target.DataContext = null;
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_DataValidation.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_DataValidation.cs
new file mode 100644
index 0000000000..5dd8d0cdf9
--- /dev/null
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_DataValidation.cs
@@ -0,0 +1,75 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Reactive.Linq;
+using Avalonia.Controls;
+using Avalonia.Data;
+using Avalonia.Markup.Data;
+using Avalonia.Markup.Xaml.Data;
+using Xunit;
+
+namespace Avalonia.Markup.Xaml.UnitTests.Data
+{
+ public class BindingTests_DataValidation
+ {
+ [Fact]
+ public void Initiate_Should_Not_Enable_Data_Validation_With_BindingPriority_LocalValue()
+ {
+ var textBlock = new TextBlock
+ {
+ DataContext = new Class1(),
+ };
+
+ var target = new Binding(nameof(Class1.Foo));
+ var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: false);
+ var subject = (BindingExpression)instanced.Subject;
+ object result = null;
+
+ subject.Subscribe(x => result = x);
+
+ Assert.IsType(result);
+ }
+
+ [Fact]
+ public void Initiate_Should_Enable_Data_Validation_With_BindingPriority_LocalValue()
+ {
+ var textBlock = new TextBlock
+ {
+ DataContext = new Class1(),
+ };
+
+ var target = new Binding(nameof(Class1.Foo));
+ var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: true);
+ var subject = (BindingExpression)instanced.Subject;
+ object result = null;
+
+ subject.Subscribe(x => result = x);
+
+ Assert.Equal(new BindingNotification("foo"), result);
+ }
+
+ [Fact]
+ public void Initiate_Should_Not_Enable_Data_Validation_With_BindingPriority_TemplatedParent()
+ {
+ var textBlock = new TextBlock
+ {
+ DataContext = new Class1(),
+ };
+
+ var target = new Binding(nameof(Class1.Foo)) { Priority = BindingPriority.TemplatedParent };
+ var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: true);
+ var subject = (BindingExpression)instanced.Subject;
+ object result = null;
+
+ subject.Subscribe(x => result = x);
+
+ Assert.IsType(result);
+ }
+
+ private class Class1
+ {
+ public string Foo { get; set; } = "foo";
+ }
+ }
+}
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Validation.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Validation.cs
index 0fed786f07..8759cb42c5 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Validation.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Validation.cs
@@ -1,7 +1,11 @@
using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Markup.Xaml.Data;
+using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Data
@@ -9,143 +13,112 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
public class BindingTests_Validation
{
[Fact]
- public void Disabled_Validation_Should_Trigger_Validation_Change_On_Exception()
+ public void Non_Validated_Property_Does_Not_Receive_BindingNotifications()
{
var source = new ValidationTestModel { MustBePositive = 5 };
- var target = new TestControl { DataContext = source };
- var binding = new Binding
+ var target = new TestControl
{
- Path = nameof(source.MustBePositive),
- Mode = BindingMode.TwoWay,
-
- // Even though EnableValidation = false, exception validation is enabled.
- EnableValidation = false,
+ DataContext = source,
+ [!TestControl.NonValidatedProperty] = new Binding(nameof(source.MustBePositive)),
};
- target.Bind(TestControl.ValidationTestProperty, binding);
-
- target.ValidationTest = -5;
-
- Assert.False(target.ValidationStatus.IsValid);
+ Assert.Empty(target.Notifications);
}
[Fact]
- public void Enabled_Validation_Should_Trigger_Validation_Change_On_Exception()
+ public void Validated_Direct_Property_Receives_BindingNotifications()
{
var source = new ValidationTestModel { MustBePositive = 5 };
- var target = new TestControl { DataContext = source };
- var binding = new Binding
+ var target = new TestControl
{
- Path = nameof(source.MustBePositive),
- Mode = BindingMode.TwoWay,
- EnableValidation = true,
+ DataContext = source,
};
- target.Bind(TestControl.ValidationTestProperty, binding);
-
- target.ValidationTest = -5;
- Assert.False(target.ValidationStatus.IsValid);
- }
-
+ target.Bind(
+ TestControl.ValidatedDirectProperty,
+ new Binding(nameof(source.MustBePositive), BindingMode.TwoWay));
- [Fact]
- public void Passed_Validation_Should_Not_Add_Invalid_Pseudo_Class()
- {
- var control = new TestControl();
- var model = new ValidationTestModel { MustBePositive = 1 };
- var binding = new Binding
- {
- Path = nameof(model.MustBePositive),
- Mode = BindingMode.TwoWay,
- EnableValidation = true,
- };
+ target.ValidatedDirect = 6;
+ target.ValidatedDirect = -1;
+ target.ValidatedDirect = 7;
- control.Bind(TestControl.ValidationTestProperty, binding);
- control.DataContext = model;
- Assert.DoesNotContain(control.Classes, x => x == ":invalid");
+ Assert.Equal(
+ new[]
+ {
+ new BindingNotification(5),
+ new BindingNotification(6),
+ new BindingNotification(new ArgumentOutOfRangeException("value"), BindingErrorType.DataValidationError),
+ new BindingNotification(7),
+ },
+ target.Notifications.AsEnumerable());
}
- [Fact]
- public void Failed_Validation_Should_Add_Invalid_Pseudo_Class()
+ private class TestControl : Control
{
- var control = new TestControl();
- var model = new ValidationTestModel { MustBePositive = 1 };
- var binding = new Binding
+ public static readonly StyledProperty NonValidatedProperty =
+ AvaloniaProperty.Register(
+ nameof(Validated),
+ enableDataValidation: false);
+
+ public static readonly StyledProperty ValidatedProperty =
+ AvaloniaProperty.Register(
+ nameof(Validated),
+ enableDataValidation: true);
+
+ public static readonly DirectProperty ValidatedDirectProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(Validated),
+ o => o.ValidatedDirect,
+ (o, v) => o.ValidatedDirect = v,
+ enableDataValidation: true);
+
+ private int _direct;
+
+ public int NonValidated
{
- Path = nameof(model.MustBePositive),
- Mode = BindingMode.TwoWay,
- EnableValidation = true,
- };
-
- control.Bind(TestControl.ValidationTestProperty, binding);
- control.DataContext = model;
- control.ValidationTest = -5;
- Assert.Contains(control.Classes, x => x == ":invalid");
- }
-
- [Fact]
- public void Failed_Then_Passed_Validation_Should_Remove_Invalid_Pseudo_Class()
- {
- var control = new TestControl();
- var model = new ValidationTestModel { MustBePositive = 1 };
+ get { return GetValue(NonValidatedProperty); }
+ set { SetValue(NonValidatedProperty, value); }
+ }
- var binding = new Binding
+ public int Validated
{
- Path = nameof(model.MustBePositive),
- Mode = BindingMode.TwoWay,
- EnableValidation = true,
- };
-
- control.Bind(TestControl.ValidationTestProperty, binding);
- control.DataContext = model;
-
-
- control.ValidationTest = -5;
- Assert.Contains(control.Classes, x => x == ":invalid");
- control.ValidationTest = 5;
- Assert.DoesNotContain(control.Classes, x => x == ":invalid");
- }
-
- private class TestControl : Control
- {
- public static readonly StyledProperty ValidationTestProperty
- = AvaloniaProperty.Register(nameof(ValidationTest), 1, defaultBindingMode: BindingMode.TwoWay);
+ get { return GetValue(ValidatedProperty); }
+ set { SetValue(ValidatedProperty, value); }
+ }
- public int ValidationTest
+ public int ValidatedDirect
{
- get
- {
- return GetValue(ValidationTestProperty);
- }
- set
- {
- SetValue(ValidationTestProperty, value);
- }
+ get { return _direct; }
+ set { SetAndRaise(ValidatedDirectProperty, ref _direct, value); }
}
- protected override void DataValidationChanged(AvaloniaProperty property, IValidationStatus status)
+ public IList Notifications { get; } = new List();
+
+ protected override void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification)
{
- if (property == ValidationTestProperty)
- {
- UpdateValidationState(status);
- }
+ Notifications.Add(notification);
}
}
- private class ValidationTestModel
+ private class ValidationTestModel : NotifyingBase
{
- private int mustBePositive;
+ private int _mustBePositive;
public int MustBePositive
{
- get { return mustBePositive; }
+ get { return _mustBePositive; }
set
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value));
}
- mustBePositive = value;
+
+ if (_mustBePositive != value)
+ {
+ _mustBePositive = value;
+ RaisePropertyChanged();
+ }
}
}
}
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs
index df62a1ed41..0c2151850f 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs
@@ -39,7 +39,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
{
if (level == LogEventLevel.Error &&
area == LogArea.Binding &&
- mt == "Error binding to {Target}.{Property}: {Message}" &&
+ mt == "Error in binding to {Target}.{Property}: {Message}" &&
pv.Length == 3 &&
pv[0] is ProgressBar &&
object.ReferenceEquals(pv[1], ProgressBar.ValueProperty) &&
diff --git a/tests/Avalonia.RenderTests/Shapes/PathTests.cs b/tests/Avalonia.RenderTests/Shapes/PathTests.cs
index 9250b6a8a0..2e5d528bce 100644
--- a/tests/Avalonia.RenderTests/Shapes/PathTests.cs
+++ b/tests/Avalonia.RenderTests/Shapes/PathTests.cs
@@ -24,6 +24,260 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes
{
}
+#if AVALONIA_CAIRO
+ [Fact(Skip = "Broken in Cairo: waiting for Skia")]
+#else
+ [Fact]
+#endif
+ public void Line_Absolute()
+ {
+ Decorator target = new Decorator
+ {
+ Width = 200,
+ Height = 200,
+ Child = new Path
+ {
+ Stroke = Brushes.Red,
+ StrokeThickness = 1,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center,
+ Data = StreamGeometry.Parse("M 10,190 L 190,10 M0,0M200,200"),
+ }
+ };
+
+ RenderToFile(target);
+ CompareImages();
+ }
+
+#if AVALONIA_CAIRO
+ [Fact(Skip = "Broken in Cairo: waiting for Skia")]
+#else
+ [Fact]
+#endif
+ public void Line_Relative()
+ {
+ Decorator target = new Decorator
+ {
+ Width = 200,
+ Height = 200,
+ Child = new Path
+ {
+ Stroke = Brushes.Red,
+ StrokeThickness = 1,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center,
+ Data = StreamGeometry.Parse("M10,190 l190,-190 M0,0M200,200"),
+ }
+ };
+
+ RenderToFile(target);
+ CompareImages();
+ }
+
+#if AVALONIA_CAIRO
+ [Fact(Skip = "Broken in Cairo: waiting for Skia")]
+#else
+ [Fact]
+#endif
+ public void HorizontalLine_Absolute()
+ {
+ Decorator target = new Decorator
+ {
+ Width = 200,
+ Height = 200,
+ Child = new Path
+ {
+ Stroke = Brushes.Red,
+ StrokeThickness = 1,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center,
+ Data = StreamGeometry.Parse("M190,100 H10 M0,0M200,200"),
+ }
+ };
+
+ RenderToFile(target);
+ CompareImages();
+ }
+
+#if AVALONIA_CAIRO
+ [Fact(Skip = "Broken in Cairo: waiting for Skia")]
+#else
+ [Fact]
+#endif
+ public void HorizontalLine_Relative()
+ {
+ Decorator target = new Decorator
+ {
+ Width = 200,
+ Height = 200,
+ Child = new Path
+ {
+ Stroke = Brushes.Red,
+ StrokeThickness = 1,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center,
+ Data = StreamGeometry.Parse("M190,100 h-180 M0,0M200,200"),
+ }
+ };
+
+ RenderToFile(target);
+ CompareImages();
+ }
+
+#if AVALONIA_CAIRO
+ [Fact(Skip = "Broken in Cairo: waiting for Skia")]
+#else
+ [Fact]
+#endif
+ public void VerticalLine_Absolute()
+ {
+ Decorator target = new Decorator
+ {
+ Width = 200,
+ Height = 200,
+ Child = new Path
+ {
+ Stroke = Brushes.Red,
+ StrokeThickness = 1,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center,
+ Data = StreamGeometry.Parse("M100,190 V10 M0,0M200,200"),
+ }
+ };
+
+ RenderToFile(target);
+ CompareImages();
+ }
+
+#if AVALONIA_CAIRO
+ [Fact(Skip = "Broken in Cairo: waiting for Skia")]
+#else
+ [Fact]
+#endif
+ public void VerticalLine_Relative()
+ {
+ Decorator target = new Decorator
+ {
+ Width = 200,
+ Height = 200,
+ Child = new Path
+ {
+ Stroke = Brushes.Red,
+ StrokeThickness = 1,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center,
+ Data = StreamGeometry.Parse("M100,190 V-180 M0,0M200,200"),
+ }
+ };
+
+ RenderToFile(target);
+ CompareImages();
+ }
+
+#if AVALONIA_CAIRO
+ [Fact(Skip = "Broken in Cairo: waiting for Skia")]
+#else
+ [Fact]
+#endif
+ public void CubicBezier_Absolute()
+ {
+ Decorator target = new Decorator
+ {
+ Width = 200,
+ Height = 200,
+ Child = new Path
+ {
+ Fill = Brushes.Gray,
+ Stroke = Brushes.Red,
+ StrokeThickness = 1,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center,
+ Data = StreamGeometry.Parse("M190,0 C10,10 190,190 10,190 M0,0M200,200"),
+ }
+ };
+
+ RenderToFile(target);
+ CompareImages();
+ }
+
+#if AVALONIA_CAIRO
+ [Fact(Skip = "Broken in Cairo: waiting for Skia")]
+#else
+ [Fact]
+#endif
+ public void CubicBezier_Relative()
+ {
+ Decorator target = new Decorator
+ {
+ Width = 200,
+ Height = 200,
+ Child = new Path
+ {
+ Fill = Brushes.Gray,
+ Stroke = Brushes.Red,
+ StrokeThickness = 1,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center,
+ Data = StreamGeometry.Parse("M190,0 c-180,10 0,190 -180,190 M0,0M200,200"),
+ }
+ };
+
+ RenderToFile(target);
+ CompareImages();
+ }
+
+#if AVALONIA_CAIRO
+ [Fact(Skip = "Broken in Cairo: waiting for Skia")]
+#else
+ [Fact]
+#endif
+ public void Arc_Absolute()
+ {
+ Decorator target = new Decorator
+ {
+ Width = 200,
+ Height = 200,
+ Child = new Path
+ {
+ Fill = Brushes.Gray,
+ Stroke = Brushes.Red,
+ StrokeThickness = 1,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center,
+ Data = StreamGeometry.Parse("M190,100 A90,90 0 1,0 10,100 M0,0M200,200"),
+ }
+ };
+
+ RenderToFile(target);
+ CompareImages();
+ }
+
+#if AVALONIA_CAIRO
+ [Fact(Skip = "Broken in Cairo: waiting for Skia")]
+#else
+ [Fact]
+#endif
+ public void Arc_Relative()
+ {
+ Decorator target = new Decorator
+ {
+ Width = 200,
+ Height = 200,
+ Child = new Path
+ {
+ Fill = Brushes.Gray,
+ Stroke = Brushes.Red,
+ StrokeThickness = 1,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center,
+ Data = StreamGeometry.Parse("M190,100 a90,90 0 1,0 -180,0 M0,0M200,200"),
+ }
+ };
+
+ RenderToFile(target);
+ CompareImages();
+ }
+
[Fact]
public void Path_100px_Triangle_Centered()
{
diff --git a/tests/Avalonia.Styling.UnitTests/SetterTests.cs b/tests/Avalonia.Styling.UnitTests/SetterTests.cs
index 4de90b7790..84536fa47b 100644
--- a/tests/Avalonia.Styling.UnitTests/SetterTests.cs
+++ b/tests/Avalonia.Styling.UnitTests/SetterTests.cs
@@ -27,7 +27,7 @@ namespace Avalonia.Styling.UnitTests
var control = new TextBlock();
var subject = new BehaviorSubject("foo");
var descriptor = new InstancedBinding(subject);
- var binding = Mock.Of(x => x.Initiate(control, TextBlock.TextProperty, null) == descriptor);
+ var binding = Mock.Of(x => x.Initiate(control, TextBlock.TextProperty, null, false) == descriptor);
var style = Mock.Of();
var setter = new Setter(TextBlock.TextProperty, binding);
diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs
index 5111680873..8f75ec8efc 100644
--- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs
+++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs
@@ -20,7 +20,7 @@ namespace Avalonia.UnitTests
return _windowImpl?.Invoke() ?? Mock.Of(x => x.Scaling == 1);
}
- public IWindowImpl CreateEmbeddableWindow()
+ public IEmbeddableWindowImpl CreateEmbeddableWindow()
{
throw new NotImplementedException();
}
diff --git a/tests/Avalonia.UnitTests/NotifyingBase.cs b/tests/Avalonia.UnitTests/NotifyingBase.cs
index c1b7a24303..c91e55d34f 100644
--- a/tests/Avalonia.UnitTests/NotifyingBase.cs
+++ b/tests/Avalonia.UnitTests/NotifyingBase.cs
@@ -16,7 +16,7 @@ namespace Avalonia.UnitTests
add
{
_propertyChanged += value;
- ++SubscriptionCount;
+ ++PropertyChangedSubscriptionCount;
}
remove
@@ -24,12 +24,12 @@ namespace Avalonia.UnitTests
if (_propertyChanged?.GetInvocationList().Contains(value) == true)
{
_propertyChanged -= value;
- --SubscriptionCount;
+ --PropertyChangedSubscriptionCount;
}
}
}
- public int SubscriptionCount
+ public int PropertyChangedSubscriptionCount
{
get;
private set;
diff --git a/tests/TestFiles/Cairo/Shapes/Path/Arc_Absolute.expected.png b/tests/TestFiles/Cairo/Shapes/Path/Arc_Absolute.expected.png
new file mode 100644
index 0000000000..113cc908e6
Binary files /dev/null and b/tests/TestFiles/Cairo/Shapes/Path/Arc_Absolute.expected.png differ
diff --git a/tests/TestFiles/Cairo/Shapes/Path/Arc_Relative.expected.png b/tests/TestFiles/Cairo/Shapes/Path/Arc_Relative.expected.png
new file mode 100644
index 0000000000..113cc908e6
Binary files /dev/null and b/tests/TestFiles/Cairo/Shapes/Path/Arc_Relative.expected.png differ
diff --git a/tests/TestFiles/Cairo/Shapes/Path/CubicBezier_Absolute.expected.png b/tests/TestFiles/Cairo/Shapes/Path/CubicBezier_Absolute.expected.png
new file mode 100644
index 0000000000..8c8f5350ee
Binary files /dev/null and b/tests/TestFiles/Cairo/Shapes/Path/CubicBezier_Absolute.expected.png differ
diff --git a/tests/TestFiles/Cairo/Shapes/Path/CubicBezier_Relative.expected.png b/tests/TestFiles/Cairo/Shapes/Path/CubicBezier_Relative.expected.png
new file mode 100644
index 0000000000..8c8f5350ee
Binary files /dev/null and b/tests/TestFiles/Cairo/Shapes/Path/CubicBezier_Relative.expected.png differ
diff --git a/tests/TestFiles/Cairo/Shapes/Path/HorizontalLine_Absolute.expected.png b/tests/TestFiles/Cairo/Shapes/Path/HorizontalLine_Absolute.expected.png
new file mode 100644
index 0000000000..546ffec3bb
Binary files /dev/null and b/tests/TestFiles/Cairo/Shapes/Path/HorizontalLine_Absolute.expected.png differ
diff --git a/tests/TestFiles/Cairo/Shapes/Path/HorizontalLine_Relative.expected.png b/tests/TestFiles/Cairo/Shapes/Path/HorizontalLine_Relative.expected.png
new file mode 100644
index 0000000000..546ffec3bb
Binary files /dev/null and b/tests/TestFiles/Cairo/Shapes/Path/HorizontalLine_Relative.expected.png differ
diff --git a/tests/TestFiles/Cairo/Shapes/Path/Line_Absolute.expected.png b/tests/TestFiles/Cairo/Shapes/Path/Line_Absolute.expected.png
new file mode 100644
index 0000000000..4661a6a8c4
Binary files /dev/null and b/tests/TestFiles/Cairo/Shapes/Path/Line_Absolute.expected.png differ
diff --git a/tests/TestFiles/Cairo/Shapes/Path/VerticalLine_Absolute.expected.png b/tests/TestFiles/Cairo/Shapes/Path/VerticalLine_Absolute.expected.png
new file mode 100644
index 0000000000..b2e2beafd4
Binary files /dev/null and b/tests/TestFiles/Cairo/Shapes/Path/VerticalLine_Absolute.expected.png differ
diff --git a/tests/TestFiles/Cairo/Shapes/Path/VerticalLine_Relative.expected.png b/tests/TestFiles/Cairo/Shapes/Path/VerticalLine_Relative.expected.png
new file mode 100644
index 0000000000..ff36de2a5c
Binary files /dev/null and b/tests/TestFiles/Cairo/Shapes/Path/VerticalLine_Relative.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Shapes/Path/Arc_Absolute.expected.png b/tests/TestFiles/Direct2D1/Shapes/Path/Arc_Absolute.expected.png
new file mode 100644
index 0000000000..113cc908e6
Binary files /dev/null and b/tests/TestFiles/Direct2D1/Shapes/Path/Arc_Absolute.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Shapes/Path/Arc_Relative.expected.png b/tests/TestFiles/Direct2D1/Shapes/Path/Arc_Relative.expected.png
new file mode 100644
index 0000000000..113cc908e6
Binary files /dev/null and b/tests/TestFiles/Direct2D1/Shapes/Path/Arc_Relative.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Shapes/Path/CubicBezier_Absolute.expected.png b/tests/TestFiles/Direct2D1/Shapes/Path/CubicBezier_Absolute.expected.png
new file mode 100644
index 0000000000..8c8f5350ee
Binary files /dev/null and b/tests/TestFiles/Direct2D1/Shapes/Path/CubicBezier_Absolute.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Shapes/Path/CubicBezier_Relative.expected.png b/tests/TestFiles/Direct2D1/Shapes/Path/CubicBezier_Relative.expected.png
new file mode 100644
index 0000000000..8c8f5350ee
Binary files /dev/null and b/tests/TestFiles/Direct2D1/Shapes/Path/CubicBezier_Relative.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Shapes/Path/HorizontalLine_Absolute.expected.png b/tests/TestFiles/Direct2D1/Shapes/Path/HorizontalLine_Absolute.expected.png
new file mode 100644
index 0000000000..546ffec3bb
Binary files /dev/null and b/tests/TestFiles/Direct2D1/Shapes/Path/HorizontalLine_Absolute.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Shapes/Path/HorizontalLine_Relative.expected.png b/tests/TestFiles/Direct2D1/Shapes/Path/HorizontalLine_Relative.expected.png
new file mode 100644
index 0000000000..546ffec3bb
Binary files /dev/null and b/tests/TestFiles/Direct2D1/Shapes/Path/HorizontalLine_Relative.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Shapes/Path/Line_Absolute.expected.png b/tests/TestFiles/Direct2D1/Shapes/Path/Line_Absolute.expected.png
new file mode 100644
index 0000000000..4661a6a8c4
Binary files /dev/null and b/tests/TestFiles/Direct2D1/Shapes/Path/Line_Absolute.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Shapes/Path/Line_Relative.expected.png b/tests/TestFiles/Direct2D1/Shapes/Path/Line_Relative.expected.png
new file mode 100644
index 0000000000..4661a6a8c4
Binary files /dev/null and b/tests/TestFiles/Direct2D1/Shapes/Path/Line_Relative.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Shapes/Path/VerticalLine_Absolute.expected.png b/tests/TestFiles/Direct2D1/Shapes/Path/VerticalLine_Absolute.expected.png
new file mode 100644
index 0000000000..b2e2beafd4
Binary files /dev/null and b/tests/TestFiles/Direct2D1/Shapes/Path/VerticalLine_Absolute.expected.png differ
diff --git a/tests/TestFiles/Direct2D1/Shapes/Path/VerticalLine_Relative.expected.png b/tests/TestFiles/Direct2D1/Shapes/Path/VerticalLine_Relative.expected.png
new file mode 100644
index 0000000000..ff36de2a5c
Binary files /dev/null and b/tests/TestFiles/Direct2D1/Shapes/Path/VerticalLine_Relative.expected.png differ
diff --git a/tests/TestFiles/Skia/Shapes/Path/Arc_Absolute.expected.png b/tests/TestFiles/Skia/Shapes/Path/Arc_Absolute.expected.png
new file mode 100644
index 0000000000..113cc908e6
Binary files /dev/null and b/tests/TestFiles/Skia/Shapes/Path/Arc_Absolute.expected.png differ
diff --git a/tests/TestFiles/Skia/Shapes/Path/Arc_Relative.expected.png b/tests/TestFiles/Skia/Shapes/Path/Arc_Relative.expected.png
new file mode 100644
index 0000000000..113cc908e6
Binary files /dev/null and b/tests/TestFiles/Skia/Shapes/Path/Arc_Relative.expected.png differ
diff --git a/tests/TestFiles/Skia/Shapes/Path/CubicBezier_Absolute.expected.png b/tests/TestFiles/Skia/Shapes/Path/CubicBezier_Absolute.expected.png
new file mode 100644
index 0000000000..8c8f5350ee
Binary files /dev/null and b/tests/TestFiles/Skia/Shapes/Path/CubicBezier_Absolute.expected.png differ
diff --git a/tests/TestFiles/Skia/Shapes/Path/CubicBezier_Relative.expected.png b/tests/TestFiles/Skia/Shapes/Path/CubicBezier_Relative.expected.png
new file mode 100644
index 0000000000..8c8f5350ee
Binary files /dev/null and b/tests/TestFiles/Skia/Shapes/Path/CubicBezier_Relative.expected.png differ
diff --git a/tests/TestFiles/Skia/Shapes/Path/HorizontalLine_Absolute.expected.png b/tests/TestFiles/Skia/Shapes/Path/HorizontalLine_Absolute.expected.png
new file mode 100644
index 0000000000..546ffec3bb
Binary files /dev/null and b/tests/TestFiles/Skia/Shapes/Path/HorizontalLine_Absolute.expected.png differ
diff --git a/tests/TestFiles/Skia/Shapes/Path/HorizontalLine_Relative.expected.png b/tests/TestFiles/Skia/Shapes/Path/HorizontalLine_Relative.expected.png
new file mode 100644
index 0000000000..546ffec3bb
Binary files /dev/null and b/tests/TestFiles/Skia/Shapes/Path/HorizontalLine_Relative.expected.png differ
diff --git a/tests/TestFiles/Skia/Shapes/Path/Line_Absolute.expected.png b/tests/TestFiles/Skia/Shapes/Path/Line_Absolute.expected.png
new file mode 100644
index 0000000000..4661a6a8c4
Binary files /dev/null and b/tests/TestFiles/Skia/Shapes/Path/Line_Absolute.expected.png differ
diff --git a/tests/TestFiles/Skia/Shapes/Path/Line_Relative.expected.png b/tests/TestFiles/Skia/Shapes/Path/Line_Relative.expected.png
new file mode 100644
index 0000000000..4661a6a8c4
Binary files /dev/null and b/tests/TestFiles/Skia/Shapes/Path/Line_Relative.expected.png differ
diff --git a/tests/TestFiles/Skia/Shapes/Path/VerticalLine_Absolute.expected.png b/tests/TestFiles/Skia/Shapes/Path/VerticalLine_Absolute.expected.png
new file mode 100644
index 0000000000..b2e2beafd4
Binary files /dev/null and b/tests/TestFiles/Skia/Shapes/Path/VerticalLine_Absolute.expected.png differ
diff --git a/tests/TestFiles/Skia/Shapes/Path/VerticalLine_Relative.expected.png b/tests/TestFiles/Skia/Shapes/Path/VerticalLine_Relative.expected.png
new file mode 100644
index 0000000000..ff36de2a5c
Binary files /dev/null and b/tests/TestFiles/Skia/Shapes/Path/VerticalLine_Relative.expected.png differ