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 87f733b370..bc60e5eb65 100644
--- a/src/Gtk/Avalonia.Gtk/WindowImpl.cs
+++ b/src/Gtk/Avalonia.Gtk/WindowImpl.cs
@@ -1,165 +1,78 @@
-// 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;
+<<<<<<< HEAD
private Point _lastPosition;
private Gtk.IMContext _imContext;
+=======
+>>>>>>> dfd4bbf34a7632465a84126b22495072945fa7d5
- 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;
_lastPosition = Position;
}
-
- 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;
}
+<<<<<<< HEAD
IntPtr IPlatformHandle.Handle => GetNativeWindow();
public string HandleDescriptor => "HWND";
@@ -193,75 +106,52 @@ namespace Avalonia.Gtk
}
public Point PointToClient(Point point)
+=======
+ void OnFocusActivated(object sender, EventArgs eventArgs)
+>>>>>>> dfd4bbf34a7632465a84126b22495072945fa7d5
{
- 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;
}
+<<<<<<< HEAD
public void SetSystemDecorations(bool enabled) => Decorated = enabled;
void ITopLevelImpl.Activate()
@@ -408,10 +298,13 @@ namespace Avalonia.Gtk
Input(e);
return true;
}
+=======
+ public override void SetSystemDecorations(bool enabled) => Window.Decorated = enabled;
+>>>>>>> dfd4bbf34a7632465a84126b22495072945fa7d5
- 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..759d35b1fc
--- /dev/null
+++ b/src/Gtk/Avalonia.Gtk/WindowImplBase.cs
@@ -0,0 +1,312 @@
+// 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 PositionChanged { 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/WindowImpl_BASE_46644.cs.orig b/src/Gtk/Avalonia.Gtk/WindowImpl_BASE_46644.cs.orig
new file mode 100644
index 0000000000..26ad52135d
--- /dev/null
+++ b/src/Gtk/Avalonia.Gtk/WindowImpl_BASE_46644.cs.orig
@@ -0,0 +1,404 @@
+// 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 class WindowImpl : Gtk.Window, IWindowImpl, IPlatformHandle
+ {
+ private IInputRoot _inputRoot;
+
+ 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)
+ {
+ DefaultSize = new Gdk.Size(900, 480);
+ Init();
+ }
+
+ public WindowImpl(Gtk.WindowType type)
+ : base(type)
+ {
+ Init();
+ }
+
+ private void Init()
+ {
+ Events = EventMask.PointerMotionMask |
+ EventMask.ButtonPressMask |
+ EventMask.ButtonReleaseMask;
+ _imContext = new Gtk.IMMulticontext();
+ _imContext.Commit += ImContext_Commit;
+ DoubleBuffered = false;
+ Realize();
+ _lastClientSize = ClientSize;
+ }
+
+ protected override void OnRealized ()
+ {
+ base.OnRealized ();
+ _imContext.ClientWindow = this.GdkWindow;
+ }
+
+ public Size ClientSize
+ {
+ get
+ {
+ int width;
+ int height;
+ GetSize(out width, out height);
+ return new Size(width, height);
+ }
+
+ set
+ {
+ Resize((int)value.Width, (int)value.Height);
+ }
+ }
+
+ public Size MaxClientSize
+ {
+ get
+ {
+ // TODO: This should take into account things such as taskbar and window border
+ // thickness etc.
+ return new Size(Screen.Width, Screen.Height);
+ }
+ }
+
+ public Avalonia.Controls.WindowState WindowState
+ {
+ 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;
+ }
+ }
+
+ 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;
+ }
+ }
+ }
+
+ 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()
+ {
+ 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);
+ }
+
+
+ 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)
+ {
+ 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;
+ }
+
+ public void BeginMoveDrag()
+ {
+ int x, y;
+ ModifierType mod;
+ Screen.RootWindow.GetPointer(out x, out y, out mod);
+ BeginMoveDrag(1, x, y, 0);
+ }
+
+ public void BeginResizeDrag(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);
+ }
+
+ public Point Position
+ {
+ get
+ {
+ int x, y;
+ GetPosition(out x, out y);
+ return new Point(x, y);
+ }
+ set
+ {
+ Move((int)value.X, (int)value.Y);
+ }
+ }
+
+ public IDisposable ShowDialog()
+ {
+ Modal = true;
+ 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 void SetIcon(IWindowIconImpl icon)
+ {
+ Icon = ((IconImpl)icon).Pixbuf;
+ }
+ }
+}
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/Skia/Avalonia.Skia.Android/Avalonia.Skia.Android.csproj b/src/Skia/Avalonia.Skia.Android/Avalonia.Skia.Android.csproj
index a40ee92fbf..4a4b280771 100644
--- a/src/Skia/Avalonia.Skia.Android/Avalonia.Skia.Android.csproj
+++ b/src/Skia/Avalonia.Skia.Android/Avalonia.Skia.Android.csproj
@@ -39,8 +39,8 @@
-
- ..\..\..\packages\SkiaSharp.1.53.0\lib\MonoAndroid\SkiaSharp.dll
+
+ ..\..\..\packages\SkiaSharp.1.54.0\lib\MonoAndroid\SkiaSharp.dll
True
diff --git a/src/Skia/Avalonia.Skia.Android/packages.config b/src/Skia/Avalonia.Skia.Android/packages.config
index 5f27ea1039..616c52f1d5 100644
--- a/src/Skia/Avalonia.Skia.Android/packages.config
+++ b/src/Skia/Avalonia.Skia.Android/packages.config
@@ -1,4 +1,4 @@
-
+
\ No newline at end of file
diff --git a/src/Skia/Avalonia.Skia.Desktop/Avalonia.Skia.Desktop.csproj b/src/Skia/Avalonia.Skia.Desktop/Avalonia.Skia.Desktop.csproj
index 8326df9881..95063bafce 100644
--- a/src/Skia/Avalonia.Skia.Desktop/Avalonia.Skia.Desktop.csproj
+++ b/src/Skia/Avalonia.Skia.Desktop/Avalonia.Skia.Desktop.csproj
@@ -56,8 +56,8 @@
MinimumRecommendedRules.ruleset
-
- ..\..\..\packages\SkiaSharp.1.53.0\lib\net45\SkiaSharp.dll
+
+ ..\..\..\packages\SkiaSharp.1.54.0\lib\net45\SkiaSharp.dll
True
@@ -116,12 +116,12 @@
-
+
This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
-
+