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 StringValueCommand { get; } - public ExceptionPropertyErrorViewModel ExceptionPropertyValidation { get; } - = new ExceptionPropertyErrorViewModel(); + public DataAnnotationsErrorViewModel DataAnnotationsValidation { get; } = new DataAnnotationsErrorViewModel(); + public ExceptionErrorViewModel ExceptionDataValidation { get; } = new ExceptionErrorViewModel(); + public IndeiErrorViewModel IndeiDataValidation { get; } = new IndeiErrorViewModel(); } } diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index daa8b0901a..63c82d9e8a 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -38,7 +38,7 @@ Designer - + Designer @@ -88,6 +88,9 @@ App.xaml + + MainView.xaml + MainWindow.xaml @@ -215,6 +218,13 @@ Designer + + + MSBuild:Compile + + + + + \ No newline at end of file diff --git a/samples/interop/GtkInteropDemo/GtkInteropDemo.v2.ncrunchproject b/samples/interop/GtkInteropDemo/GtkInteropDemo.v2.ncrunchproject new file mode 100644 index 0000000000..e1b4d7cf28 --- /dev/null +++ b/samples/interop/GtkInteropDemo/GtkInteropDemo.v2.ncrunchproject @@ -0,0 +1,26 @@ + + true + 1000 + false + false + false + true + false + false + true + false + false + false + true + false + true + true + true + 60000 + + + + AutoDetect + STA + x86 + \ No newline at end of file diff --git a/samples/interop/GtkInteropDemo/MainWindow.cs b/samples/interop/GtkInteropDemo/MainWindow.cs new file mode 100644 index 0000000000..2c193f24e8 --- /dev/null +++ b/samples/interop/GtkInteropDemo/MainWindow.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Diagnostics; +using Avalonia.Gtk.Embedding; +using ControlCatalog; +using Gtk; + +namespace GtkInteropDemo +{ + class MainWindow : Window + { + public MainWindow() : base("Gtk Embedding Demo") + { + var root = new HBox(); + var left = new VBox(); + left.Add(new Button("I'm GTK button")); + left.Add(new Calendar()); + root.PackEnd(left, false, false, 0); + var host = new GtkAvaloniaControlHost() {Content = new MainView()}; + host.SetSizeRequest(600, 600); + root.PackStart(host, true, true, 0); + Add(root); + + ShowAll(); + } + } +} diff --git a/samples/interop/GtkInteropDemo/Program.cs b/samples/interop/GtkInteropDemo/Program.cs new file mode 100644 index 0000000000..7dd39c74ca --- /dev/null +++ b/samples/interop/GtkInteropDemo/Program.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using ControlCatalog; + +namespace GtkInteropDemo +{ + static class Program + { + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() + { + AppBuilder.Configure().UseGtk().UseCairo().SetupWithoutStarting(); + new MainWindow().Show(); + Gtk.Application.Run(); + } + } +} diff --git a/samples/interop/GtkInteropDemo/Properties/AssemblyInfo.cs b/samples/interop/GtkInteropDemo/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..8aaf28c2f0 --- /dev/null +++ b/samples/interop/GtkInteropDemo/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("GtkInteropDemo")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("GtkInteropDemo")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("bd7f352c-6dc1-4740-baf2-2d34a038728c")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/samples/interop/GtkInteropDemo/Properties/Resources.Designer.cs b/samples/interop/GtkInteropDemo/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..f79a8bf680 --- /dev/null +++ b/samples/interop/GtkInteropDemo/Properties/Resources.Designer.cs @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace GtkInteropDemo.Properties +{ + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources + { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() + { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if ((resourceMan == null)) + { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("GtkInteropDemo.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture + { + get + { + return resourceCulture; + } + set + { + resourceCulture = value; + } + } + } +} diff --git a/samples/interop/GtkInteropDemo/Properties/Resources.resx b/samples/interop/GtkInteropDemo/Properties/Resources.resx new file mode 100644 index 0000000000..af7dbebbac --- /dev/null +++ b/samples/interop/GtkInteropDemo/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/samples/interop/GtkInteropDemo/Properties/Settings.Designer.cs b/samples/interop/GtkInteropDemo/Properties/Settings.Designer.cs new file mode 100644 index 0000000000..8173ee7dfa --- /dev/null +++ b/samples/interop/GtkInteropDemo/Properties/Settings.Designer.cs @@ -0,0 +1,30 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace GtkInteropDemo.Properties +{ + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase + { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default + { + get + { + return defaultInstance; + } + } + } +} diff --git a/samples/interop/GtkInteropDemo/Properties/Settings.settings b/samples/interop/GtkInteropDemo/Properties/Settings.settings new file mode 100644 index 0000000000..39645652af --- /dev/null +++ b/samples/interop/GtkInteropDemo/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + diff --git a/samples/interop/WindowsInteropTest/App.config b/samples/interop/WindowsInteropTest/App.config new file mode 100644 index 0000000000..8324aa6ff1 --- /dev/null +++ b/samples/interop/WindowsInteropTest/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/samples/interop/WindowsInteropTest/EmbedToWinFormsDemo.Designer.cs b/samples/interop/WindowsInteropTest/EmbedToWinFormsDemo.Designer.cs new file mode 100644 index 0000000000..b5dccffd4d --- /dev/null +++ b/samples/interop/WindowsInteropTest/EmbedToWinFormsDemo.Designer.cs @@ -0,0 +1,119 @@ +using Avalonia.Win32.Embedding; + +namespace WindowsInteropTest +{ + partial class EmbedToWinFormsDemo + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.button1 = new System.Windows.Forms.Button(); + this.monthCalendar1 = new System.Windows.Forms.MonthCalendar(); + this.groupBox1 = new System.Windows.Forms.GroupBox(); + this.groupBox2 = new System.Windows.Forms.GroupBox(); + this.avaloniaHost = new WinFormsAvaloniaControlHost(); + this.groupBox1.SuspendLayout(); + this.groupBox2.SuspendLayout(); + this.SuspendLayout(); + // + // button1 + // + this.button1.Location = new System.Drawing.Point(28, 29); + this.button1.Name = "button1"; + this.button1.Size = new System.Drawing.Size(164, 73); + this.button1.TabIndex = 0; + this.button1.Text = "button1"; + this.button1.UseVisualStyleBackColor = true; + // + // monthCalendar1 + // + this.monthCalendar1.Location = new System.Drawing.Point(28, 114); + this.monthCalendar1.Name = "monthCalendar1"; + this.monthCalendar1.TabIndex = 1; + // + // groupBox1 + // + this.groupBox1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left))); + this.groupBox1.Controls.Add(this.button1); + this.groupBox1.Controls.Add(this.monthCalendar1); + this.groupBox1.Location = new System.Drawing.Point(12, 12); + this.groupBox1.Name = "groupBox1"; + this.groupBox1.Size = new System.Drawing.Size(227, 418); + this.groupBox1.TabIndex = 2; + this.groupBox1.TabStop = false; + this.groupBox1.Text = "WinForms"; + // + // groupBox2 + // + this.groupBox2.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.groupBox2.Controls.Add(this.avaloniaHost); + this.groupBox2.Location = new System.Drawing.Point(245, 12); + this.groupBox2.Name = "groupBox2"; + this.groupBox2.Size = new System.Drawing.Size(501, 418); + this.groupBox2.TabIndex = 3; + this.groupBox2.TabStop = false; + this.groupBox2.Text = "Avalonia"; + // + // avaloniaHost + // + this.avaloniaHost.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.avaloniaHost.Content = null; + this.avaloniaHost.Location = new System.Drawing.Point(6, 19); + this.avaloniaHost.Name = "avaloniaHost"; + this.avaloniaHost.Size = new System.Drawing.Size(489, 393); + this.avaloniaHost.TabIndex = 0; + this.avaloniaHost.Text = "avaloniaHost"; + // + // EmbedToWinFormsDemo + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(758, 442); + this.Controls.Add(this.groupBox2); + this.Controls.Add(this.groupBox1); + this.MinimumSize = new System.Drawing.Size(600, 400); + this.Name = "EmbedToWinFormsDemo"; + this.Text = "EmbedToWinFormsDemo"; + this.groupBox1.ResumeLayout(false); + this.groupBox2.ResumeLayout(false); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.Button button1; + private System.Windows.Forms.MonthCalendar monthCalendar1; + private System.Windows.Forms.GroupBox groupBox1; + private System.Windows.Forms.GroupBox groupBox2; + private WinFormsAvaloniaControlHost avaloniaHost; + } +} \ No newline at end of file diff --git a/samples/interop/WindowsInteropTest/EmbedToWinFormsDemo.cs b/samples/interop/WindowsInteropTest/EmbedToWinFormsDemo.cs new file mode 100644 index 0000000000..bc627f57ce --- /dev/null +++ b/samples/interop/WindowsInteropTest/EmbedToWinFormsDemo.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; +using Avalonia.Controls; +using ControlCatalog; + +namespace WindowsInteropTest +{ + public partial class EmbedToWinFormsDemo : Form + { + public EmbedToWinFormsDemo() + { + InitializeComponent(); + avaloniaHost.Content = new MainView(); + } + } +} diff --git a/samples/interop/WindowsInteropTest/EmbedToWinFormsDemo.resx b/samples/interop/WindowsInteropTest/EmbedToWinFormsDemo.resx new file mode 100644 index 0000000000..1af7de150c --- /dev/null +++ b/samples/interop/WindowsInteropTest/EmbedToWinFormsDemo.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/samples/interop/WindowsInteropTest/EmbedToWpfDemo.xaml b/samples/interop/WindowsInteropTest/EmbedToWpfDemo.xaml new file mode 100644 index 0000000000..1115cf5768 --- /dev/null +++ b/samples/interop/WindowsInteropTest/EmbedToWpfDemo.xaml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + diff --git a/samples/interop/WindowsInteropTest/EmbedToWpfDemo.xaml.cs b/samples/interop/WindowsInteropTest/EmbedToWpfDemo.xaml.cs new file mode 100644 index 0000000000..e60c9ced0a --- /dev/null +++ b/samples/interop/WindowsInteropTest/EmbedToWpfDemo.xaml.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; +using Avalonia.Controls; +using ControlCatalog; +using Window = System.Windows.Window; + +namespace WindowsInteropTest +{ + /// + /// Interaction logic for EmbedToWpfDemo.xaml + /// + public partial class EmbedToWpfDemo : Window + { + public EmbedToWpfDemo() + { + InitializeComponent(); + Host.Content = new MainView(); + } + } +} diff --git a/samples/interop/WindowsInteropTest/Program.cs b/samples/interop/WindowsInteropTest/Program.cs new file mode 100644 index 0000000000..4770688ecf --- /dev/null +++ b/samples/interop/WindowsInteropTest/Program.cs @@ -0,0 +1,22 @@ +using System; +using Avalonia.Controls; +using ControlCatalog; +using Avalonia; + +namespace WindowsInteropTest +{ + static class Program + { + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() + { + System.Windows.Forms.Application.EnableVisualStyles(); + System.Windows.Forms.Application.SetCompatibleTextRenderingDefault(false); + AppBuilder.Configure().UseWin32().UseSkia().SetupWithoutStarting(); + System.Windows.Forms.Application.Run(new SelectorForm()); + } + } +} diff --git a/samples/interop/WindowsInteropTest/Properties/AssemblyInfo.cs b/samples/interop/WindowsInteropTest/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..087fdfe991 --- /dev/null +++ b/samples/interop/WindowsInteropTest/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("WindowsInteropTest")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("WindowsInteropTest")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("c7a69145-60b6-4882-97d6-a3921dd43978")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/samples/interop/WindowsInteropTest/Properties/Resources.Designer.cs b/samples/interop/WindowsInteropTest/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..c3223efc62 --- /dev/null +++ b/samples/interop/WindowsInteropTest/Properties/Resources.Designer.cs @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace WindowsInteropTest.Properties +{ + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources + { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() + { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if ((resourceMan == null)) + { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("WindowsInteropTest.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture + { + get + { + return resourceCulture; + } + set + { + resourceCulture = value; + } + } + } +} diff --git a/samples/interop/WindowsInteropTest/Properties/Resources.resx b/samples/interop/WindowsInteropTest/Properties/Resources.resx new file mode 100644 index 0000000000..af7dbebbac --- /dev/null +++ b/samples/interop/WindowsInteropTest/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/samples/interop/WindowsInteropTest/Properties/Settings.Designer.cs b/samples/interop/WindowsInteropTest/Properties/Settings.Designer.cs new file mode 100644 index 0000000000..4b768fb74f --- /dev/null +++ b/samples/interop/WindowsInteropTest/Properties/Settings.Designer.cs @@ -0,0 +1,30 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace WindowsInteropTest.Properties +{ + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase + { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default + { + get + { + return defaultInstance; + } + } + } +} diff --git a/samples/interop/WindowsInteropTest/Properties/Settings.settings b/samples/interop/WindowsInteropTest/Properties/Settings.settings new file mode 100644 index 0000000000..39645652af --- /dev/null +++ b/samples/interop/WindowsInteropTest/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + diff --git a/samples/interop/WindowsInteropTest/SelectorForm.Designer.cs b/samples/interop/WindowsInteropTest/SelectorForm.Designer.cs new file mode 100644 index 0000000000..7cf402913e --- /dev/null +++ b/samples/interop/WindowsInteropTest/SelectorForm.Designer.cs @@ -0,0 +1,76 @@ +namespace WindowsInteropTest +{ + partial class SelectorForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.btnEmbedToWinForms = new System.Windows.Forms.Button(); + this.btnEmbedToWpf = new System.Windows.Forms.Button(); + this.SuspendLayout(); + // + // btnEmbedToWinForms + // + this.btnEmbedToWinForms.Location = new System.Drawing.Point(12, 12); + this.btnEmbedToWinForms.Name = "btnEmbedToWinForms"; + this.btnEmbedToWinForms.Size = new System.Drawing.Size(201, 86); + this.btnEmbedToWinForms.TabIndex = 0; + this.btnEmbedToWinForms.Text = "Embed to WinForms"; + this.btnEmbedToWinForms.UseVisualStyleBackColor = true; + this.btnEmbedToWinForms.Click += new System.EventHandler(this.btnEmbedToWinForms_Click); + // + // btnEmbedToWpf + // + this.btnEmbedToWpf.Location = new System.Drawing.Point(219, 12); + this.btnEmbedToWpf.Name = "btnEmbedToWpf"; + this.btnEmbedToWpf.Size = new System.Drawing.Size(201, 86); + this.btnEmbedToWpf.TabIndex = 1; + this.btnEmbedToWpf.Text = "Embed to WPF"; + this.btnEmbedToWpf.UseVisualStyleBackColor = true; + this.btnEmbedToWpf.Click += new System.EventHandler(this.btnEmbedToWpf_Click); + // + // SelectorForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(432, 284); + this.Controls.Add(this.btnEmbedToWpf); + this.Controls.Add(this.btnEmbedToWinForms); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; + this.MaximizeBox = false; + this.Name = "SelectorForm"; + this.Text = "Interop"; + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.Button btnEmbedToWinForms; + private System.Windows.Forms.Button btnEmbedToWpf; + } +} + diff --git a/samples/interop/WindowsInteropTest/SelectorForm.cs b/samples/interop/WindowsInteropTest/SelectorForm.cs new file mode 100644 index 0000000000..6430694f79 --- /dev/null +++ b/samples/interop/WindowsInteropTest/SelectorForm.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace WindowsInteropTest +{ + public partial class SelectorForm : Form + { + public SelectorForm() + { + InitializeComponent(); + } + + private void btnEmbedToWinForms_Click(object sender, EventArgs e) + { + new EmbedToWinFormsDemo().ShowDialog(this); + } + + private void btnEmbedToWpf_Click(object sender, EventArgs e) + { + new EmbedToWpfDemo().ShowDialog(); + } + } +} diff --git a/samples/interop/WindowsInteropTest/SelectorForm.resx b/samples/interop/WindowsInteropTest/SelectorForm.resx new file mode 100644 index 0000000000..1af7de150c --- /dev/null +++ b/samples/interop/WindowsInteropTest/SelectorForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj b/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj new file mode 100644 index 0000000000..827f73d7ae --- /dev/null +++ b/samples/interop/WindowsInteropTest/WindowsInteropTest.csproj @@ -0,0 +1,190 @@ + + + + + Debug + AnyCPU + {C7A69145-60B6-4882-97D6-A3921DD43978} + WinExe + Properties + WindowsInteropTest + WindowsInteropTest + v4.6 + 512 + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + Form + + + EmbedToWinFormsDemo.cs + + + EmbedToWpfDemo.xaml + + + Form + + + SelectorForm.cs + + + + + EmbedToWinFormsDemo.cs + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + True + Resources.resx + + + SelectorForm.cs + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + True + Settings.settings + True + + + + + + + + {d211e587-d8bc-45b9-95a4-f297c8fa5200} + Avalonia.Animation + + + {b09b78d8-9b26-48b0-9149-d64a2f120f3f} + Avalonia.Base + + + {d2221c82-4a25-4583-9b43-d791e3f6820c} + Avalonia.Controls + + + {799a7bb5-3c2c-48b6-85a7-406a12c420da} + Avalonia.DesignerSupport + + + {7062ae20-5dcc-4442-9645-8195bdece63e} + Avalonia.Diagnostics + + + {4a1abb09-9047-4bd5-a4ad-a055e52c5ee0} + Avalonia.DotNetFrameworkRuntime + + + {62024b2d-53eb-4638-b26b-85eeaa54866e} + Avalonia.Input + + + {6b0ed19d-a08b-461c-a9d9-a9ee40b0c06b} + Avalonia.Interactivity + + + {42472427-4774-4c81-8aff-9f27b8e31721} + Avalonia.Layout + + + {6417b24e-49c2-4985-8db2-3ab9d898ec91} + Avalonia.ReactiveUI + + + {eb582467-6abb-43a1-b052-e981ba910e3a} + Avalonia.SceneGraph + + + {f1baa01a-f176-4c6a-b39d-5b40bb1b148f} + Avalonia.Styling + + + {3e10a5fa-e8da-48b1-ad44-6a5b6cb7750f} + Avalonia.Themes.Default + + + {3e53a01a-b331-47f3-b828-4a5717e77a24} + Avalonia.Markup.Xaml + + + {6417e941-21bc-467b-a771-0de389353ce6} + Avalonia.Markup + + + {925dd807-b651-475f-9f7c-cbeb974ce43d} + Avalonia.Skia.Desktop + + + {3e908f67-5543-4879-a1dc-08eace79b3cd} + Avalonia.Direct2D1 + + + {811a76cf-1cf6-440f-963b-bbe31bd72a82} + Avalonia.Win32 + + + {d0a739b9-3c68-4ba6-a328-41606954b6bd} + ControlCatalog + + + + + Designer + MSBuild:Compile + + + + + \ No newline at end of file diff --git a/samples/interop/WindowsInteropTest/WindowsInteropTest.v2.ncrunchproject b/samples/interop/WindowsInteropTest/WindowsInteropTest.v2.ncrunchproject new file mode 100644 index 0000000000..e1b4d7cf28 --- /dev/null +++ b/samples/interop/WindowsInteropTest/WindowsInteropTest.v2.ncrunchproject @@ -0,0 +1,26 @@ + + true + 1000 + false + false + false + true + false + false + true + false + false + false + true + false + true + true + true + 60000 + + + + AutoDetect + STA + x86 + \ No newline at end of file diff --git a/src/Android/Avalonia.Android/AndroidPlatform.cs b/src/Android/Avalonia.Android/AndroidPlatform.cs index 7ab6bf8a3c..1a6624e8f6 100644 --- a/src/Android/Avalonia.Android/AndroidPlatform.cs +++ b/src/Android/Avalonia.Android/AndroidPlatform.cs @@ -57,7 +57,7 @@ namespace Avalonia.Android return new WindowImpl(); } - public IWindowImpl CreateEmbeddableWindow() + public IEmbeddableWindowImpl CreateEmbeddableWindow() { throw new NotImplementedException(); } diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index 1dfc5cf65b..887d3ff9bd 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -43,10 +43,9 @@ Properties\SharedAssemblyInfo.cs - + + - - @@ -116,9 +115,11 @@ + + diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index e6b88c71f8..eeaf782e83 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -50,29 +50,6 @@ namespace Avalonia /// private EventHandler _propertyChanged; - /// - /// Defines the property. - /// - public static readonly DirectProperty ValidationStatusProperty = - AvaloniaProperty.RegisterDirect(nameof(ValidationStatus), c => c.ValidationStatus); - - private ObjectValidationStatus validationStatus; - - /// - /// The current validation status of the control. - /// - public ObjectValidationStatus ValidationStatus - { - get - { - return validationStatus; - } - private set - { - SetAndRaise(ValidationStatusProperty, ref validationStatus, value); - } - } - /// /// Initializes a new instance of the class. /// @@ -251,6 +228,10 @@ namespace Avalonia /// /// The property. /// True if the property is set, otherwise false. + /// + /// Checks whether a value is assigned to the property, or that there is a binding to the + /// property that is producing a value other than . + /// public bool IsSet(AvaloniaProperty property) { Contract.Requires(property != null); @@ -281,42 +262,11 @@ namespace Avalonia if (property.IsDirect) { - var accessor = (IDirectPropertyAccessor)GetRegistered(property); - LogPropertySet(property, value, priority); - accessor.SetValue(this, DirectUnsetToDefault(value, property)); + SetDirectValue(property, value); } else { - PriorityValue v; - var originalValue = value; - - if (!AvaloniaPropertyRegistry.Instance.IsRegistered(this, property)) - { - ThrowNotRegistered(property); - } - - if (!TypeUtilities.TryCast(property.PropertyType, value, out value)) - { - throw new ArgumentException(string.Format( - "Invalid value for Property '{0}': '{1}' ({2})", - property.Name, - originalValue, - originalValue?.GetType().FullName ?? "(null)")); - } - - if (!_values.TryGetValue(property, out v)) - { - if (value == AvaloniaProperty.UnsetValue) - { - return; - } - - v = CreatePriorityValue(property); - _values.Add(property, v); - } - - LogPropertySet(property, value, priority); - v.SetValue(value, (int)priority); + SetStyledValue(property, value, priority); } } @@ -371,7 +321,6 @@ namespace Avalonia GetDescription(source)); IDisposable subscription = null; - IDisposable validationSubcription = null; if (_directBindings == null) { @@ -379,19 +328,14 @@ namespace Avalonia } subscription = source - .Where(x => !(x is IValidationStatus)) .Select(x => CastOrDefault(x, property.PropertyType)) .Do(_ => { }, () => _directBindings.Remove(subscription)) - .Subscribe(x => DirectBindingSet(property, x)); - validationSubcription = source - .OfType() - .Subscribe(x => DataValidationChanged(property, x)); + .Subscribe(x => SetDirectValue(property, x)); _directBindings.Add(subscription); return Disposable.Create(() => { - validationSubcription.Dispose(); subscription.Dispose(); _directBindings.Remove(subscription); }); @@ -487,28 +431,9 @@ namespace Avalonia } /// - void IPriorityValueOwner.DataValidationChanged(PriorityValue sender, IValidationStatus status) + void IPriorityValueOwner.BindingNotificationReceived(PriorityValue sender, BindingNotification notification) { - var property = sender.Property; - DataValidationChanged(property, status); - } - - /// - /// Called when the validation state on a tracked property is changed. - /// - /// The property whose validation state changed. - /// The new validation state. - protected virtual void DataValidationChanged(AvaloniaProperty property, IValidationStatus status) - { - } - - /// - /// Updates the validation status of the current object. - /// - /// The new validation status. - protected void UpdateValidationState(IValidationStatus status) - { - ValidationStatus = ValidationStatus.UpdateValidationStatus(status); + UpdateDataValidation(sender.Property, notification); } /// @@ -542,6 +467,18 @@ namespace Avalonia }); } + /// + /// Called to update the validation state for properties for which data validation is + /// enabled. + /// + /// The property. + /// The new validation status. + protected virtual void UpdateDataValidation( + AvaloniaProperty property, + BindingNotification status) + { + } + /// /// Called when a avalonia property changes on the object. /// @@ -623,22 +560,27 @@ namespace Avalonia /// /// Tries to cast a value to a type, taking into account that the value may be a - /// . + /// . /// /// The value. /// The type. - /// The cast value, or a . + /// The cast value, or a . private static object CastOrDefault(object value, Type type) { - var error = value as BindingError; + var notification = value as BindingNotification; - if (error == null) + if (notification == null) { return TypeUtilities.CastOrDefault(value, type); } else { - return error; + if (notification.HasValue) + { + notification.SetValue(TypeUtilities.CastOrDefault(notification.Value, type)); + } + + return notification; } } @@ -666,50 +608,6 @@ namespace Avalonia return result; } - /// - /// Sets a property value for a direct property binding. - /// - /// The property. - /// The value. - /// - private void DirectBindingSet(AvaloniaProperty property, object value) - { - var error = value as BindingError; - - if (error == null) - { - SetValue(property, value); - } - else - { - if (error.UseFallbackValue) - { - SetValue(property, error.FallbackValue); - } - - Logger.Error( - LogArea.Binding, - this, - "Error binding to {Target}.{Property}: {Message}", - this, - property, - error.Exception.Message); - } - } - - /// - /// Converts an unset value to the default value for a direct property. - /// - /// The value. - /// The property. - /// The value. - private object DirectUnsetToDefault(object value, AvaloniaProperty property) - { - return value == AvaloniaProperty.UnsetValue ? - ((IDirectPropertyMetadata)property.GetMetadata(GetType())).UnsetValue : - value; - } - /// /// Gets the default value for a property. /// @@ -753,6 +651,109 @@ namespace Avalonia return result; } + /// + /// Sets the value of a direct property. + /// + /// The property. + /// The value. + private void SetDirectValue(AvaloniaProperty property, object value) + { + var notification = value as BindingNotification; + + if (notification != null) + { + if (notification.ErrorType == BindingErrorType.Error) + { + Logger.Error( + LogArea.Binding, + this, + "Error in binding to {Target}.{Property}: {Message}", + this, + property, + ExceptionUtilities.GetMessage(notification.Error)); + } + + if (notification.HasValue) + { + value = notification.Value; + } + } + + if (notification == null || notification.HasValue) + { + var metadata = (IDirectPropertyMetadata)property.GetMetadata(GetType()); + var accessor = (IDirectPropertyAccessor)GetRegistered(property); + var finalValue = value == AvaloniaProperty.UnsetValue ? + metadata.UnsetValue : value; + + LogPropertySet(property, value, BindingPriority.LocalValue); + + accessor.SetValue(this, finalValue); + } + + if (notification != null) + { + UpdateDataValidation(property, notification); + } + } + + /// + /// Sets the value of a styled property. + /// + /// The property. + /// The value. + /// The priority of the value. + private void SetStyledValue(AvaloniaProperty property, object value, BindingPriority priority) + { + var notification = value as BindingNotification; + + // We currently accept BindingNotifications for non-direct properties but we just + // strip them to their underlying value. + if (notification != null) + { + if (!notification.HasValue) + { + return; + } + else + { + value = notification.Value; + } + } + + var originalValue = value; + + if (!AvaloniaPropertyRegistry.Instance.IsRegistered(this, property)) + { + ThrowNotRegistered(property); + } + + if (!TypeUtilities.TryCast(property.PropertyType, value, out value)) + { + throw new ArgumentException(string.Format( + "Invalid value for Property '{0}': '{1}' ({2})", + property.Name, + originalValue, + originalValue?.GetType().FullName ?? "(null)")); + } + + PriorityValue v; + + if (!_values.TryGetValue(property, out v)) + { + if (value == AvaloniaProperty.UnsetValue) + { + return; + } + + v = CreatePriorityValue(property); + _values.Add(property, v); + } + + LogPropertySet(property, value, priority); + v.SetValue(value, (int)priority); + } + /// /// Given a returns a registered avalonia property that is /// equal or throws if not found. diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index 2751d8d5d5..3ca55529e6 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -216,7 +216,13 @@ namespace Avalonia Contract.Requires(property != null); Contract.Requires(binding != null); - var result = binding.Initiate(target, property, anchor); + var metadata = property.GetMetadata(target.GetType()) as IDirectPropertyMetadata; + + var result = binding.Initiate( + target, + property, + anchor, + metadata?.EnableDataValidation ?? false); if (result != null) { @@ -311,7 +317,8 @@ namespace Avalonia public InstancedBinding Initiate( IAvaloniaObject target, AvaloniaProperty targetProperty, - object anchor = null) + object anchor = null, + bool enableDataValidation = false) { return new InstancedBinding(_source); } diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 0f5500b116..61006b1173 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -360,20 +360,25 @@ namespace Avalonia /// The value to use when the property is set to /// /// The default binding mode for the property. + /// + /// Whether the property is interested in data validation. + /// /// A public static DirectProperty RegisterDirect( string name, Func getter, Action setter = null, TValue unsetValue = default(TValue), - BindingMode defaultBindingMode = BindingMode.OneWay) + BindingMode defaultBindingMode = BindingMode.OneWay, + bool enableDataValidation = false) where TOwner : IAvaloniaObject { Contract.Requires(name != null); var metadata = new DirectPropertyMetadata( unsetValue: unsetValue, - defaultBindingMode: defaultBindingMode); + defaultBindingMode: defaultBindingMode, + enableDataValidation: enableDataValidation); var result = new DirectProperty(name, getter, setter, metadata); AvaloniaPropertyRegistry.Instance.Register(typeof(TOwner), result); diff --git a/src/Avalonia.Base/Data/BindingChainNullException.cs b/src/Avalonia.Base/Data/BindingChainNullException.cs new file mode 100644 index 0000000000..0e50a36d8a --- /dev/null +++ b/src/Avalonia.Base/Data/BindingChainNullException.cs @@ -0,0 +1,85 @@ +// 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.Data +{ + /// + /// An exception returned through signalling that a + /// requested binding expression could not be evaluated because of a null in one of the links + /// of the binding chain. + /// + public class BindingChainNullException : Exception + { + private string _message; + + /// + /// Initalizes a new instance of the class. + /// + public BindingChainNullException() + { + } + + /// + /// Initalizes a new instance of the class. + /// + public BindingChainNullException(string message) + { + _message = message; + } + + /// + /// Initalizes a new instance of the class. + /// + /// The expression. + /// + /// The point in the expression at which the null was encountered. + /// + public BindingChainNullException(string expression, string expressionNullPoint) + { + Expression = expression; + ExpressionNullPoint = expressionNullPoint; + } + + /// + /// Gets the expression that could not be evaluated. + /// + public string Expression { get; protected set; } + + /// + /// Gets the point in the expression at which the null was encountered. + /// + public string ExpressionNullPoint { get; protected set; } + + /// + public override string Message + { + get + { + if (_message == null) + { + _message = BuildMessage(); + } + + return _message; + } + } + + private string BuildMessage() + { + if (Expression != null && ExpressionNullPoint != null) + { + return $"'{ExpressionNullPoint}' is null in expression '{Expression}'."; + } + else if (ExpressionNullPoint != null) + { + return $"'{ExpressionNullPoint}' is null in expression."; + } + else + { + return "Null encountered in binding expression."; + } + } + } +} diff --git a/src/Avalonia.Base/Data/BindingError.cs b/src/Avalonia.Base/Data/BindingError.cs deleted file mode 100644 index 5157586a49..0000000000 --- a/src/Avalonia.Base/Data/BindingError.cs +++ /dev/null @@ -1,59 +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; - -namespace Avalonia.Data -{ - /// - /// Represents a recoverable binding error. - /// - /// - /// When produced by a binding source observable, informs the binding system that an error - /// occurred. It can also provide an optional fallback value to be pushed to the binding - /// target. - /// - /// Instead of using , one could simply not push a value (in the - /// case of a no fallback value) or push a fallback value, but BindingError also causes an - /// error to be logged with the correct binding target. - /// - public class BindingError - { - /// - /// Initializes a new instance of the class. - /// - /// An exception describing the binding error. - public BindingError(Exception exception) - { - Exception = exception; - } - - /// - /// Initializes a new instance of the class. - /// - /// An exception describing the binding error. - /// The fallback value. - public BindingError(Exception exception, object fallbackValue) - { - Exception = exception; - FallbackValue = fallbackValue; - UseFallbackValue = true; - } - - /// - /// Gets the exception describing the binding error. - /// - public Exception Exception { get; } - - /// - /// Get the fallback value. - /// - public object FallbackValue { get; } - - /// - /// Get a value indicating whether the fallback value should be pushed to the binding - /// target. - /// - public bool UseFallbackValue { get; } - } -} diff --git a/src/Avalonia.Base/Data/BindingNotification.cs b/src/Avalonia.Base/Data/BindingNotification.cs new file mode 100644 index 0000000000..ecaf59e174 --- /dev/null +++ b/src/Avalonia.Base/Data/BindingNotification.cs @@ -0,0 +1,282 @@ +// 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.Data +{ + /// + /// Defines the types of binding errors for a . + /// + public enum BindingErrorType + { + /// + /// There was no error. + /// + None, + + /// + /// There was a binding error. + /// + Error, + + /// + /// There was a data validation error. + /// + DataValidationError, + } + + /// + /// Represents a binding notification that can be a valid binding value, or a binding or + /// data validation error. + /// + public class BindingNotification + { + /// + /// A binding notification representing the null value. + /// + public static readonly BindingNotification Null = + new BindingNotification(null); + + /// + /// A binding notification representing . + /// + public static readonly BindingNotification UnsetValue = + new BindingNotification(AvaloniaProperty.UnsetValue); + + // Null cannot be held in WeakReference as it's indistinguishable from an expired value so + // use this value in its place. + private static readonly object NullValue = new object(); + + private WeakReference _value; + + /// + /// Initializes a new instance of the class. + /// + /// The binding value. + public BindingNotification(object value) + { + _value = new WeakReference(value ?? NullValue); + } + + /// + /// Initializes a new instance of the class. + /// + /// The binding error. + /// The type of the binding error. + public BindingNotification(Exception error, BindingErrorType errorType) + { + if (errorType == BindingErrorType.None) + { + throw new ArgumentException($"'errorType' may not be None"); + } + + Error = error; + ErrorType = errorType; + } + + /// + /// Initializes a new instance of the class. + /// + /// The binding error. + /// The type of the binding error. + /// The fallback value. + public BindingNotification(Exception error, BindingErrorType errorType, object fallbackValue) + : this(error, errorType) + { + _value = new WeakReference(fallbackValue ?? NullValue); + } + + /// + /// Gets the value that should be passed to the target when + /// is true. + /// + /// + /// If this property is read when is false then it will return + /// . + /// + public object Value + { + get + { + if (_value != null) + { + object result; + + if (_value.TryGetTarget(out result)) + { + return result == NullValue ? null : result; + } + } + + // There's the possibility of a race condition in that HasValue can return true, + // and then the value is GC'd before Value is read. We should be ok though as + // we return UnsetValue which should be a safe alternative. + return AvaloniaProperty.UnsetValue; + } + } + + /// + /// Gets a value indicating whether should be pushed to the target. + /// + public bool HasValue => _value != null; + + /// + /// Gets the error that occurred on the source, if any. + /// + public Exception Error { get; set; } + + /// + /// Gets the type of error that represents, if any. + /// + public BindingErrorType ErrorType { get; set; } + + /// + /// Compares two instances of for equality. + /// + /// The first instance. + /// The second instance. + /// true if the two instances are equal; otherwise false. + public static bool operator ==(BindingNotification a, BindingNotification b) + { + if (object.ReferenceEquals(a, b)) + { + return true; + } + + if ((object)a == null || (object)b == null) + { + return false; + } + + return a.HasValue == b.HasValue && + a.ErrorType == b.ErrorType && + (!a.HasValue || object.Equals(a.Value, b.Value)) && + (a.ErrorType == BindingErrorType.None || ExceptionEquals(a.Error, b.Error)); + } + + /// + /// Compares two instances of for inequality. + /// + /// The first instance. + /// The second instance. + /// true if the two instances are unequal; otherwise false. + public static bool operator !=(BindingNotification a, BindingNotification b) + { + return !(a == b); + } + + /// + /// Gets a value from an object that may be a . + /// + /// The object. + /// The value. + /// + /// If is a then returns the binding + /// notification's . If not, returns the object unchanged. + /// + public static object ExtractValue(object o) + { + var notification = o as BindingNotification; + return notification != null ? notification.Value : o; + } + + /// + /// Gets an exception from an object that may be a . + /// + /// The object. + /// The value. + /// + /// If is a then returns the binding + /// notification's . If not, returns the object unchanged. + /// + public static object ExtractError(object o) + { + var notification = o as BindingNotification; + return notification != null ? notification.Error : o; + } + + /// + /// Compares an object to an instance of for equality. + /// + /// The object to compare. + /// true if the two instances are equal; otherwise false. + public override bool Equals(object obj) + { + return Equals(obj as BindingNotification); + } + + /// + /// Compares a value to an instance of for equality. + /// + /// The value to compare. + /// true if the two instances are equal; otherwise false. + public bool Equals(BindingNotification other) + { + return this == other; + } + + /// + /// Gets the hash code for this instance of . + /// + /// A hash code. + public override int GetHashCode() + { + return base.GetHashCode(); + } + + /// + /// Adds an error to the . + /// + /// The error to add. + /// The error type. + public void AddError(Exception e, BindingErrorType type) + { + Contract.Requires(e != null); + Contract.Requires(type != BindingErrorType.None); + + Error = Error != null ? new AggregateException(Error, e) : e; + + if (type == BindingErrorType.Error || ErrorType == BindingErrorType.Error) + { + ErrorType = BindingErrorType.Error; + } + } + + /// + /// Removes the and makes return null. + /// + public void ClearValue() + { + _value = null; + } + + /// + /// Sets the . + /// + public void SetValue(object value) + { + _value = new WeakReference(value ?? NullValue); + } + + /// + public override string ToString() + { + switch (ErrorType) + { + case BindingErrorType.None: + return $"{{Value: {Value}}}"; + default: + return HasValue ? + $"{{{ErrorType}: {Error}, Fallback: {Value}}}" : + $"{{{ErrorType}: {Error}}}"; + } + } + + private static bool ExceptionEquals(Exception a, Exception b) + { + return a?.GetType() == b?.GetType() && + a?.Message == b?.Message; + } + } +} diff --git a/src/Avalonia.Base/Data/BindingOperations.cs b/src/Avalonia.Base/Data/BindingOperations.cs index 9899eb633c..eb7c449bec 100644 --- a/src/Avalonia.Base/Data/BindingOperations.cs +++ b/src/Avalonia.Base/Data/BindingOperations.cs @@ -54,7 +54,10 @@ namespace Avalonia.Data if (source != null) { - return source.Take(1).Subscribe(x => target.SetValue(property, x, binding.Priority)); + return source + .Where(x => BindingNotification.ExtractValue(x) != AvaloniaProperty.UnsetValue) + .Take(1) + .Subscribe(x => target.SetValue(property, x, binding.Priority)); } else { diff --git a/src/Avalonia.Base/Data/IBinding.cs b/src/Avalonia.Base/Data/IBinding.cs index 88c20dd288..70447ad3eb 100644 --- a/src/Avalonia.Base/Data/IBinding.cs +++ b/src/Avalonia.Base/Data/IBinding.cs @@ -19,12 +19,14 @@ namespace Avalonia.Data /// order to locate named controls or resources. The parameter /// can be used to provice this context. /// + /// Whether data validation should be enabled. /// /// A or null if the binding could not be resolved. /// InstancedBinding Initiate( IAvaloniaObject target, AvaloniaProperty targetProperty, - object anchor = null); + object anchor = null, + bool enableDataValidation = false); } } diff --git a/src/Avalonia.Base/Data/IValidationStatus.cs b/src/Avalonia.Base/Data/IValidationStatus.cs deleted file mode 100644 index 30a2459af0..0000000000 --- a/src/Avalonia.Base/Data/IValidationStatus.cs +++ /dev/null @@ -1,17 +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. - -namespace Avalonia.Data -{ - /// - /// Contains information on if the current object passed validation. - /// Subclasses of this class contain additional information depending on the method of validation checking. - /// - public interface IValidationStatus - { - /// - /// True when the data passes validation; otherwise, false. - /// - bool IsValid { get; } - } -} diff --git a/src/Avalonia.Base/Data/IndexerBinding.cs b/src/Avalonia.Base/Data/IndexerBinding.cs index b2d38920d0..729b21b0d9 100644 --- a/src/Avalonia.Base/Data/IndexerBinding.cs +++ b/src/Avalonia.Base/Data/IndexerBinding.cs @@ -21,7 +21,11 @@ namespace Avalonia.Data public AvaloniaProperty Property { get; } private BindingMode Mode { get; } - public InstancedBinding Initiate(IAvaloniaObject target, AvaloniaProperty targetProperty, object anchor = null) + public InstancedBinding Initiate( + IAvaloniaObject target, + AvaloniaProperty targetProperty, + object anchor = null, + bool enableDataValidation = false) { var mode = Mode == BindingMode.Default ? targetProperty.GetMetadata(target.GetType()).DefaultBindingMode : diff --git a/src/Avalonia.Base/Data/ObjectValidationStatus.cs b/src/Avalonia.Base/Data/ObjectValidationStatus.cs deleted file mode 100644 index 9ce3dcd897..0000000000 --- a/src/Avalonia.Base/Data/ObjectValidationStatus.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Avalonia.Data -{ - /// - /// An immutable struct that contains validation information for a that validates a single property. - /// - public struct ObjectValidationStatus : IValidationStatus - { - private Dictionary currentValidationStatus; - - public bool IsValid => currentValidationStatus?.Values.All(status => status.IsValid) ?? true; - - /// - /// Constructs the structure with the given validation information. - /// - /// The validation information - public ObjectValidationStatus(Dictionary validations) - :this() - { - currentValidationStatus = validations; - } - - /// - /// Creates a new status with the updated information. - /// - /// The updated status information. - /// The new validation status. - public ObjectValidationStatus UpdateValidationStatus(IValidationStatus status) - { - var newStatus = new Dictionary(currentValidationStatus ?? - new Dictionary()); - newStatus[status.GetType()] = status; - return new ObjectValidationStatus(newStatus); - } - - public IEnumerable StatusInformation => currentValidationStatus.Values; - } -} diff --git a/src/Avalonia.Base/DirectProperty.cs b/src/Avalonia.Base/DirectProperty.cs index cf325ed269..fad6cf983a 100644 --- a/src/Avalonia.Base/DirectProperty.cs +++ b/src/Avalonia.Base/DirectProperty.cs @@ -85,19 +85,26 @@ namespace Avalonia /// The value to use when the property is set to /// /// The default binding mode for the property. + /// + /// Whether the property is interested in data validation. + /// /// The property. public DirectProperty AddOwner( Func getter, Action setter = null, TValue unsetValue = default(TValue), - BindingMode defaultBindingMode = BindingMode.OneWay) + BindingMode defaultBindingMode = BindingMode.OneWay, + bool enableDataValidation = false) where TNewOwner : AvaloniaObject { var result = new DirectProperty( this, getter, setter, - new DirectPropertyMetadata(unsetValue, defaultBindingMode)); + new DirectPropertyMetadata( + unsetValue: unsetValue, + defaultBindingMode: defaultBindingMode, + enableDataValidation: enableDataValidation)); AvaloniaPropertyRegistry.Instance.Register(typeof(TNewOwner), result); return result; diff --git a/src/Avalonia.Base/DirectPropertyMetadata`1.cs b/src/Avalonia.Base/DirectPropertyMetadata`1.cs index 69dac6e8e2..d22801e35a 100644 --- a/src/Avalonia.Base/DirectPropertyMetadata`1.cs +++ b/src/Avalonia.Base/DirectPropertyMetadata`1.cs @@ -17,19 +17,35 @@ namespace Avalonia /// The value to use when the property is set to /// /// The default binding mode. + /// + /// Whether the property is interested in data validation. + /// public DirectPropertyMetadata( TValue unsetValue = default(TValue), - BindingMode defaultBindingMode = BindingMode.Default) + BindingMode defaultBindingMode = BindingMode.Default, + bool enableDataValidation = false) : base(defaultBindingMode) { UnsetValue = unsetValue; + EnableDataValidation = enableDataValidation; } /// - /// Gets the to use when the property is set to . + /// Gets the value to use when the property is set to . /// public TValue UnsetValue { get; private set; } + /// + /// Gets a value indicating whether the property is interested in data validation. + /// + /// + /// Data validation is validation performed at the target of a binding, for example in a + /// view model using the INotifyDataErrorInfo interface. Only certain properties on a + /// control (such as a TextBox's Text property) will be interested in recieving data + /// validation messages so this feature must be explicitly enabled by setting this flag. + /// + public bool EnableDataValidation { get; } + /// object IDirectPropertyMetadata.UnsetValue => UnsetValue; diff --git a/src/Avalonia.Base/IDirectPropertyMetadata.cs b/src/Avalonia.Base/IDirectPropertyMetadata.cs index e9b7603ffe..9dc014f0b8 100644 --- a/src/Avalonia.Base/IDirectPropertyMetadata.cs +++ b/src/Avalonia.Base/IDirectPropertyMetadata.cs @@ -12,5 +12,10 @@ namespace Avalonia /// Gets the to use when the property is set to . /// object UnsetValue { get; } + + /// + /// Gets a value indicating whether the property is interested in data validation. + /// + bool EnableDataValidation { get; } } } \ No newline at end of file diff --git a/src/Avalonia.Base/IPriorityValueOwner.cs b/src/Avalonia.Base/IPriorityValueOwner.cs index 743eba4453..57f98c0717 100644 --- a/src/Avalonia.Base/IPriorityValueOwner.cs +++ b/src/Avalonia.Base/IPriorityValueOwner.cs @@ -19,10 +19,11 @@ namespace Avalonia void Changed(PriorityValue sender, object oldValue, object newValue); /// - /// Called when the validation state of a changes. + /// Called when a is received by a + /// . /// /// The source of the change. - /// The validation status. - void DataValidationChanged(PriorityValue sender, IValidationStatus status); + /// The notification. + void BindingNotificationReceived(PriorityValue sender, BindingNotification notification); } } diff --git a/src/Avalonia.Base/PriorityBindingEntry.cs b/src/Avalonia.Base/PriorityBindingEntry.cs index 3368f45b82..580b593666 100644 --- a/src/Avalonia.Base/PriorityBindingEntry.cs +++ b/src/Avalonia.Base/PriorityBindingEntry.cs @@ -93,22 +93,24 @@ namespace Avalonia private void ValueChanged(object value) { - var bindingError = value as BindingError; + var notification = value as BindingNotification; - if (bindingError != null) + if (notification != null) { - _owner.Error(this, bindingError); + if (notification.HasValue) + { + Value = notification.Value; + _owner.Changed(this); + } + + if (notification.ErrorType != BindingErrorType.None) + { + _owner.Error(this, notification); + } } - - var validationStatus = value as IValidationStatus; - - if (validationStatus != null) - { - _owner.Validation(this, validationStatus); - } - else if (bindingError == null || bindingError.UseFallbackValue) + else { - Value = bindingError == null ? value : bindingError.FallbackValue; + Value = value; _owner.Changed(this); } } diff --git a/src/Avalonia.Base/PriorityLevel.cs b/src/Avalonia.Base/PriorityLevel.cs index c0460a775b..122a6df821 100644 --- a/src/Avalonia.Base/PriorityLevel.cs +++ b/src/Avalonia.Base/PriorityLevel.cs @@ -159,22 +159,11 @@ namespace Avalonia /// /// The entry that completed. /// The error. - public void Error(PriorityBindingEntry entry, BindingError error) + public void Error(PriorityBindingEntry entry, BindingNotification error) { _owner.LevelError(this, error); } - /// - /// Invoked when an entry in reports validation status. - /// - /// The entry that completed. - /// The validation status. - public void Validation(PriorityBindingEntry entry, IValidationStatus validationStatus) - { - _owner.LevelValidation(this, validationStatus); - } - - /// /// Activates the first binding that has a value. /// diff --git a/src/Avalonia.Base/PriorityValue.cs b/src/Avalonia.Base/PriorityValue.cs index 20434bc97d..a7eb4465b3 100644 --- a/src/Avalonia.Base/PriorityValue.cs +++ b/src/Avalonia.Base/PriorityValue.cs @@ -77,7 +77,6 @@ namespace Avalonia /// /// The binding. /// The binding priority. - /// Validation settings for the binding. /// /// A disposable that will remove the binding. /// @@ -179,31 +178,21 @@ namespace Avalonia } } - /// - /// Called whenever a priority level validation state changes. - /// - /// The priority level of the changed entry. - /// The validation status. - public void LevelValidation(PriorityLevel priorityLevel, IValidationStatus validationStatus) - { - _owner.DataValidationChanged(this, validationStatus); - } - /// /// Called when a priority level encounters an error. /// /// The priority level of the changed entry. /// The binding error. - public void LevelError(PriorityLevel level, BindingError error) + public void LevelError(PriorityLevel level, BindingNotification error) { Logger.Log( LogEventLevel.Error, LogArea.Binding, _owner, - "Error binding to {Target}.{Property}: {Message}", + "Error in binding to {Target}.{Property}: {Message}", _owner, Property, - error.Exception.Message); + error.Error.Message); } /// @@ -248,8 +237,14 @@ namespace Avalonia /// The priority level that the value came from. private void UpdateValue(object value, int priority) { + var notification = value as BindingNotification; object castValue; + if (notification != null) + { + value = (notification.HasValue) ? notification.Value : null; + } + if (TypeUtilities.TryCast(_valueType, value, out castValue)) { var old = _value; @@ -261,7 +256,21 @@ namespace Avalonia ValuePriority = priority; _value = castValue; - _owner?.Changed(this, old, _value); + + if (notification?.HasValue == true) + { + notification.SetValue(castValue); + } + + if (notification == null || notification.HasValue) + { + _owner?.Changed(this, old, _value); + } + + if (notification != null) + { + _owner?.BindingNotificationReceived(this, notification); + } } else { diff --git a/src/Avalonia.Base/PropertyMetadata.cs b/src/Avalonia.Base/PropertyMetadata.cs index 4cbb99f7e4..395aad53e4 100644 --- a/src/Avalonia.Base/PropertyMetadata.cs +++ b/src/Avalonia.Base/PropertyMetadata.cs @@ -17,7 +17,8 @@ namespace Avalonia /// Initializes a new instance of the class. /// /// The default binding mode. - public PropertyMetadata(BindingMode defaultBindingMode = BindingMode.Default) + public PropertyMetadata( + BindingMode defaultBindingMode = BindingMode.Default) { _defaultBindingMode = defaultBindingMode; } diff --git a/src/Avalonia.Base/Utilities/ExceptionUtilities.cs b/src/Avalonia.Base/Utilities/ExceptionUtilities.cs new file mode 100644 index 0000000000..fa8c5be788 --- /dev/null +++ b/src/Avalonia.Base/Utilities/ExceptionUtilities.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Avalonia.Utilities +{ + internal static class ExceptionUtilities + { + public static string GetMessage(Exception e) + { + var aggregate = e as AggregateException; + + if (aggregate != null) + { + return string.Join(" | ", aggregate.InnerExceptions.Select(x => x.Message)); + } + + return e.Message; + } + } +} diff --git a/src/Avalonia.Base/Utilities/TypeUtilities.cs b/src/Avalonia.Base/Utilities/TypeUtilities.cs index 2d4c911933..7295bfa7ab 100644 --- a/src/Avalonia.Base/Utilities/TypeUtilities.cs +++ b/src/Avalonia.Base/Utilities/TypeUtilities.cs @@ -27,6 +27,21 @@ namespace Avalonia.Utilities { typeof(short), new List { typeof(byte) } } }; + private static readonly Type[] NumericTypes = new[] + { + typeof(Byte), + typeof(Decimal), + typeof(Double), + typeof(Int16), + typeof(Int32), + typeof(Int64), + typeof(SByte), + typeof(Single), + typeof(UInt16), + typeof(UInt32), + typeof(UInt64), + }; + /// /// Returns a value indicating whether null can be assigned to the specified type. /// @@ -208,5 +223,31 @@ namespace Avalonia.Utilities return null; } } + + /// + /// Determines if a type is numeric. Nullable numeric types are considered numeric. + /// + /// + /// True if the type is numberic; otherwise false. + /// + /// + /// Boolean is not considered numeric. + /// + public static bool IsNumeric(Type type) + { + if (type == null) + { + return false; + } + + if (type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + return IsNumeric(Nullable.GetUnderlyingType(type)); + } + else + { + return NumericTypes.Contains(type); + } + } } } diff --git a/src/Avalonia.Base/Utilities/WeakObservable.cs b/src/Avalonia.Base/Utilities/WeakObservable.cs new file mode 100644 index 0000000000..c261cc0520 --- /dev/null +++ b/src/Avalonia.Base/Utilities/WeakObservable.cs @@ -0,0 +1,54 @@ +// 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; +using System.Reactive.Linq; + +namespace Avalonia.Utilities +{ + /// + /// Provides extension methods for working with weak event handlers. + /// + public static class WeakObservable + { + /// + /// Converts a .NET event conforming to the standard .NET event pattern into an observable + /// sequence, subscribing weakly. + /// + /// The type of the event args. + /// Object instance that exposes the event to convert. + /// Name of the event to convert. + /// + public static IObservable> FromEventPattern( + object target, + string eventName) + where TEventArgs : EventArgs + { + Contract.Requires(target != null); + Contract.Requires(eventName != null); + + return Observable.Create>(observer => + { + var handler = new Handler(observer); + WeakSubscriptionManager.Subscribe(target, eventName, handler); + return () => WeakSubscriptionManager.Unsubscribe(target, eventName, handler); + }).Publish().RefCount(); + } + + private class Handler : IWeakSubscriber where TEventArgs : EventArgs + { + private IObserver> _observer; + + public Handler(IObserver> observer) + { + _observer = observer; + } + + public void OnEvent(object sender, TEventArgs e) + { + _observer.OnNext(new EventPattern(sender, e)); + } + } + } +} diff --git a/src/Avalonia.Controls/AppBuilder.cs b/src/Avalonia.Controls/AppBuilderBase.cs similarity index 79% rename from src/Avalonia.Controls/AppBuilder.cs rename to src/Avalonia.Controls/AppBuilderBase.cs index 82698acde7..5afbb444ee 100644 --- a/src/Avalonia.Controls/AppBuilder.cs +++ b/src/Avalonia.Controls/AppBuilderBase.cs @@ -10,7 +10,8 @@ namespace Avalonia.Controls /// /// Initializes up platform-specific services for an . /// - public abstract class AppBuilderBase where AppBuilder : AppBuilderBase, new() + /// + public abstract class AppBuilderBase where TAppBuilder : AppBuilderBase, new() { /// /// Gets or sets the instance. @@ -41,7 +42,7 @@ namespace Avalonia.Controls /// Gets or sets a method to call before is called on the /// . /// - public Action BeforeStartCallback { get; set; } + public Action BeforeStartCallback { get; set; } protected AppBuilderBase(IRuntimePlatform platform, Action platformSevices) { @@ -53,8 +54,8 @@ namespace Avalonia.Controls /// Begin configuring an . /// /// The subclass of to configure. - /// An instance. - public static AppBuilder Configure() + /// An instance. + public static TAppBuilder Configure() where TApp : Application, new() { return Configure(new TApp()); @@ -63,26 +64,26 @@ namespace Avalonia.Controls /// /// Begin configuring an . /// - /// An instance. - public static AppBuilder Configure(Application app) + /// An instance. + public static TAppBuilder Configure(Application app) { AvaloniaLocator.CurrentMutable.BindToSelf(app); - return new AppBuilder() + return new TAppBuilder() { Instance = app, }; } - protected AppBuilder Self => (AppBuilder) this; + protected TAppBuilder Self => (TAppBuilder) this; /// /// Registers a callback to call before is called on the /// . /// /// The callback. - /// An instance. - public AppBuilder BeforeStarting(Action callback) + /// An instance. + public TAppBuilder BeforeStarting(Action callback) { BeforeStartCallback = callback; return Self; @@ -107,7 +108,7 @@ namespace Avalonia.Controls /// Sets up the platform-specific services for the application, but does not run it. /// /// - public AppBuilder SetupWithoutStarting() + public TAppBuilder SetupWithoutStarting() { Setup(); return Self; @@ -117,8 +118,8 @@ namespace Avalonia.Controls /// Specifies a windowing subsystem to use. /// /// The method to call to initialize the windowing subsystem. - /// An instance. - public AppBuilder UseWindowingSubsystem(Action initializer) + /// An instance. + public TAppBuilder UseWindowingSubsystem(Action initializer) { WindowingSubsystem = initializer; return Self; @@ -128,15 +129,15 @@ namespace Avalonia.Controls /// Specifies a windowing subsystem to use. /// /// The dll in which to look for subsystem. - /// An instance. - public AppBuilder UseWindowingSubsystem(string dll) => UseWindowingSubsystem(GetInitializer(dll)); + /// An instance. + public TAppBuilder UseWindowingSubsystem(string dll) => UseWindowingSubsystem(GetInitializer(dll)); /// /// Specifies a rendering subsystem to use. /// /// The method to call to initialize the rendering subsystem. - /// An instance. - public AppBuilder UseRenderingSubsystem(Action initializer) + /// An instance. + public TAppBuilder UseRenderingSubsystem(Action initializer) { RenderingSubsystem = initializer; return Self; @@ -146,8 +147,8 @@ namespace Avalonia.Controls /// Specifies a rendering subsystem to use. /// /// The dll in which to look for subsystem. - /// An instance. - public AppBuilder UseRenderingSubsystem(string dll) => UseRenderingSubsystem(GetInitializer(dll)); + /// An instance. + public TAppBuilder UseRenderingSubsystem(string dll) => UseRenderingSubsystem(GetInitializer(dll)); static Action GetInitializer(string assemblyName) => () => { diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index d6e5926727..7a04e0747c 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -43,7 +43,7 @@ Properties\SharedAssemblyInfo.cs - + @@ -56,6 +56,8 @@ + + diff --git a/src/Avalonia.Controls/Canvas.cs b/src/Avalonia.Controls/Canvas.cs index 74eebea4bc..8b433c2b67 100644 --- a/src/Avalonia.Controls/Canvas.cs +++ b/src/Avalonia.Controls/Canvas.cs @@ -47,6 +47,7 @@ namespace Avalonia.Controls /// static Canvas() { + ClipToBoundsProperty.OverrideDefaultValue(false); AffectsCanvasArrange(LeftProperty, TopProperty, RightProperty, BottomProperty); } diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 944f6b82ac..5cd2ddfc35 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -108,7 +108,6 @@ namespace Avalonia.Controls PseudoClass(IsEnabledCoreProperty, x => !x, ":disabled"); PseudoClass(IsFocusedProperty, ":focus"); PseudoClass(IsPointerOverProperty, ":pointerover"); - PseudoClass(ValidationStatusProperty, status => !status.IsValid, ":invalid"); } /// @@ -400,13 +399,6 @@ namespace Avalonia.Controls /// protected IPseudoClasses PseudoClasses => Classes; - /// - protected override void DataValidationChanged(AvaloniaProperty property, IValidationStatus status) - { - base.DataValidationChanged(property, status); - ValidationStatus.UpdateValidationStatus(status); - } - /// /// Sets the control's logical parent. /// @@ -729,7 +721,6 @@ namespace Avalonia.Controls _isAttachedToLogicalTree = false; _styleDetach.OnNext(this); - this.TemplatedParent = null; OnDetachedFromLogicalTree(e); foreach (var child in LogicalChildren.OfType()) diff --git a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs new file mode 100644 index 0000000000..fc58e751f4 --- /dev/null +++ b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs @@ -0,0 +1,73 @@ +using System; +using Avalonia.Controls.Platform; +using Avalonia.Input; +using Avalonia.Layout; +using Avalonia.Platform; +using Avalonia.Styling; + +namespace Avalonia.Controls.Embedding +{ + public class EmbeddableControlRoot : TopLevel, IStyleable, IFocusScope, INameScope, IDisposable + { + public EmbeddableControlRoot(IEmbeddableWindowImpl impl) : base(impl) + { + PlatformImpl.Show(); + } + + public EmbeddableControlRoot() : base(PlatformManager.CreateEmbeddableWindow()) + { + PlatformImpl.Show(); + } + + public new IEmbeddableWindowImpl PlatformImpl => (IEmbeddableWindowImpl) base.PlatformImpl; + + public void Prepare() + { + EnsureInitialized(); + ApplyTemplate(); + PlatformImpl.Show(); + LayoutManager.Instance.ExecuteInitialLayoutPass(this); + } + + private void EnsureInitialized() + { + if (!this.IsInitialized) + { + var init = (ISupportInitialize)this; + init.BeginInit(); + init.EndInit(); + } + } + + protected override Size MeasureOverride(Size availableSize) + { + base.MeasureOverride(PlatformImpl.ClientSize); + return PlatformImpl.ClientSize; + } + + private readonly NameScope _nameScope = new NameScope(); + public event EventHandler Registered + { + add { _nameScope.Registered += value; } + remove { _nameScope.Registered -= value; } + } + + public event EventHandler Unregistered + { + add { _nameScope.Unregistered += value; } + remove { _nameScope.Unregistered -= value; } + } + + public void Register(string name, object element) => _nameScope.Register(name, element); + + public object Find(string name) => _nameScope.Find(name); + + public void Unregister(string name) => _nameScope.Unregister(name); + + Type IStyleable.StyleKey => typeof(EmbeddableControlRoot); + public void Dispose() + { + PlatformImpl.Dispose(); + } + } +} diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 8c4f58148f..8f92cc43b7 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -285,17 +285,6 @@ namespace Avalonia.Controls LogicalChildren.RemoveAll(toRemove); } - /// - protected override void OnTemplateChanged(AvaloniaPropertyChangedEventArgs e) - { - base.OnTemplateChanged(e); - - if (e.NewValue == null) - { - ItemContainerGenerator?.Clear(); - } - } - /// /// Caled when the property changes. /// diff --git a/src/Avalonia.Controls/Platform/IEmbeddableWindowImpl.cs b/src/Avalonia.Controls/Platform/IEmbeddableWindowImpl.cs new file mode 100644 index 0000000000..45b15e5f45 --- /dev/null +++ b/src/Avalonia.Controls/Platform/IEmbeddableWindowImpl.cs @@ -0,0 +1,15 @@ +// 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.Platform +{ + /// + /// Defines a platform-specific embeddable window implementation. + /// + public interface IEmbeddableWindowImpl : IWindowImpl + { + event Action LostFocus; + } +} diff --git a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs index 86fe9b1dcf..5dcd0a39e8 100644 --- a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs +++ b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs @@ -9,7 +9,7 @@ namespace Avalonia.Platform public interface IWindowingPlatform { IWindowImpl CreateWindow(); - IWindowImpl CreateEmbeddableWindow(); + IEmbeddableWindowImpl CreateEmbeddableWindow(); IPopupImpl CreatePopup(); } } diff --git a/src/Avalonia.Controls/Platform/PlatformManager.cs b/src/Avalonia.Controls/Platform/PlatformManager.cs index e75f3c162b..e57eb21b91 100644 --- a/src/Avalonia.Controls/Platform/PlatformManager.cs +++ b/src/Avalonia.Controls/Platform/PlatformManager.cs @@ -43,6 +43,14 @@ namespace Avalonia.Controls.Platform return s_designerMode ? platform.CreateEmbeddableWindow() : platform.CreateWindow(); } + public static IEmbeddableWindowImpl CreateEmbeddableWindow() + { + var platform = AvaloniaLocator.Current.GetService(); + if (platform == null) + throw new Exception("Could not CreateEmbeddableWindow(): IWindowingPlatform is not registered."); + return platform.CreateEmbeddableWindow(); + } + public static IPopupImpl CreatePopup() { return AvaloniaLocator.Current.GetService().CreatePopup(); diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 81d01ff74f..63f3fb647b 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -68,6 +68,7 @@ namespace Avalonia.Controls.Primitives private PopupRoot _popupRoot; private TopLevel _topLevel; private IDisposable _nonClientListener; + bool _ignoreIsOpenChanged = false; /// /// Initializes static members of the class. @@ -220,7 +221,11 @@ namespace Avalonia.Controls.Primitives PopupRootCreated?.Invoke(this, EventArgs.Empty); _popupRoot.Show(); + + _ignoreIsOpenChanged = true; IsOpen = true; + _ignoreIsOpenChanged = false; + Opened?.Invoke(this, EventArgs.Empty); } @@ -268,8 +273,13 @@ namespace Avalonia.Controls.Primitives { base.OnDetachedFromLogicalTree(e); _topLevel = null; - _popupRoot?.Dispose(); - _popupRoot = null; + + if (_popupRoot != null) + { + ((ISetLogicalParent)_popupRoot).SetParent(null); + _popupRoot.Dispose(); + _popupRoot = null; + } } /// @@ -278,13 +288,16 @@ namespace Avalonia.Controls.Primitives /// The event args. private void IsOpenChanged(AvaloniaPropertyChangedEventArgs e) { - if ((bool)e.NewValue) - { - Open(); - } - else + if (!_ignoreIsOpenChanged) { - Close(); + if ((bool)e.NewValue) + { + Open(); + } + else + { + Close(); + } } } diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index 16c66c7cfc..f67bbef5e1 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -130,7 +130,7 @@ namespace Avalonia.Controls.Primitives control.ApplyTemplate(); - if (!(control is IPresenter && control.TemplatedParent == templatedParent)) + if (!(control is IPresenter) && control.TemplatedParent == templatedParent) { foreach (IControl child in control.GetVisualChildren()) { diff --git a/src/Avalonia.Controls/Properties/AssemblyInfo.cs b/src/Avalonia.Controls/Properties/AssemblyInfo.cs index f87897267b..ae8c88f7e8 100644 --- a/src/Avalonia.Controls/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Controls/Properties/AssemblyInfo.cs @@ -11,6 +11,7 @@ using Avalonia.Metadata; [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Embedding")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Presenters")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Primitives")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Shapes")] diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index a1d913d40a..5806dd5bb3 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -35,6 +35,14 @@ namespace Avalonia.Controls o => o.CaretIndex, (o, v) => o.CaretIndex = v); + public static readonly DirectProperty> DataValidationErrorsProperty = + AvaloniaProperty.RegisterDirect>( + nameof(DataValidationErrors), + o => o.DataValidationErrors); + + public static readonly StyledProperty IsReadOnlyProperty = + AvaloniaProperty.Register(nameof(IsReadOnly)); + public static readonly DirectProperty SelectionStartProperty = AvaloniaProperty.RegisterDirect( nameof(SelectionStart), @@ -51,7 +59,8 @@ namespace Avalonia.Controls TextBlock.TextProperty.AddOwner( o => o.Text, (o, v) => o.Text = v, - defaultBindingMode: BindingMode.TwoWay); + defaultBindingMode: BindingMode.TwoWay, + enableDataValidation: true); public static readonly StyledProperty TextAlignmentProperty = TextBlock.TextAlignmentProperty.AddOwner(); @@ -65,9 +74,6 @@ namespace Avalonia.Controls public static readonly StyledProperty UseFloatingWatermarkProperty = AvaloniaProperty.Register("UseFloatingWatermark"); - public static readonly StyledProperty IsReadOnlyProperty = - AvaloniaProperty.Register(nameof(IsReadOnly)); - struct UndoRedoState : IEquatable { public string Text { get; } @@ -89,6 +95,8 @@ namespace Avalonia.Controls private bool _canScrollHorizontally; private TextPresenter _presenter; private UndoRedoHelper _undoRedoHelper; + private bool _ignoreTextChanges; + private IEnumerable _dataValidationErrors; static TextBox() { @@ -145,6 +153,18 @@ namespace Avalonia.Controls } } + public IEnumerable DataValidationErrors + { + get { return _dataValidationErrors; } + private set { SetAndRaise(DataValidationErrorsProperty, ref _dataValidationErrors, value); } + } + + public bool IsReadOnly + { + get { return GetValue(IsReadOnlyProperty); } + set { SetValue(IsReadOnlyProperty, value); } + } + public int SelectionStart { get @@ -177,7 +197,13 @@ namespace Avalonia.Controls public string Text { get { return _text; } - set { SetAndRaise(TextProperty, ref _text, value); } + set + { + if (!_ignoreTextChanges) + { + SetAndRaise(TextProperty, ref _text, value); + } + } } public TextAlignment TextAlignment @@ -198,12 +224,6 @@ namespace Avalonia.Controls set { SetValue(UseFloatingWatermarkProperty, value); } } - public bool IsReadOnly - { - get { return GetValue(IsReadOnlyProperty); } - set { SetValue(IsReadOnlyProperty, value); } - } - public TextWrapping TextWrapping { get { return GetValue(TextWrappingProperty); } @@ -235,14 +255,6 @@ namespace Avalonia.Controls HandleTextInput(e.Text); } - protected override void DataValidationChanged(AvaloniaProperty property, IValidationStatus status) - { - if (property == TextProperty) - { - UpdateValidationState(status); - } - } - private void HandleTextInput(string input) { if (!IsReadOnly) @@ -254,7 +266,7 @@ namespace Avalonia.Controls DeleteSelection(); caretIndex = CaretIndex; text = Text ?? string.Empty; - Text = text.Substring(0, caretIndex) + input + text.Substring(caretIndex); + SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex)); CaretIndex += input.Length; SelectionStart = SelectionEnd = CaretIndex; _undoRedoHelper.DiscardRedo(); @@ -367,7 +379,8 @@ namespace Avalonia.Controls if (!DeleteSelection() && CaretIndex > 0) { - CaretIndex -= DeleteCharacter(CaretIndex - 1); + SetTextInternal(text.Substring(0, caretIndex - 1) + text.Substring(caretIndex)); + --CaretIndex; } break; @@ -380,7 +393,7 @@ namespace Avalonia.Controls if (!DeleteSelection() && caretIndex < text.Length) { - DeleteCharacter(CaretIndex); + SetTextInternal(text.Substring(0, caretIndex) + text.Substring(caretIndex + 1)); } break; @@ -478,6 +491,35 @@ namespace Avalonia.Controls } } + protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status) + { + if (property == TextProperty) + { + var classes = (IPseudoClasses)Classes; + DataValidationErrors = UnpackException(status.Error); + classes.Set(":error", DataValidationErrors != null); + } + } + + private static IEnumerable UnpackException(Exception exception) + { + if (exception != null) + { + var aggregate = exception as AggregateException; + var exceptions = aggregate == null ? + (IEnumerable)new[] { exception } : + aggregate.InnerExceptions; + var filtered = exceptions.Where(x => !(x is BindingChainNullException)).ToList(); + + if (filtered.Count > 0) + { + return filtered; + } + } + + return null; + } + private int CoerceCaretIndex(int value) { var text = Text; @@ -663,7 +705,7 @@ namespace Avalonia.Controls var start = Math.Min(selectionStart, selectionEnd); var end = Math.Max(selectionStart, selectionEnd); var text = Text; - Text = text.Substring(0, start) + text.Substring(end); + SetTextInternal(text.Substring(0, start) + text.Substring(end)); SelectionStart = SelectionEnd = CaretIndex = start; return true; } @@ -710,6 +752,19 @@ namespace Avalonia.Controls return i; } + private void SetTextInternal(string value) + { + try + { + _ignoreTextChanges = true; + SetAndRaise(TextProperty, ref _text, value); + } + finally + { + _ignoreTextChanges = false; + } + } + private void SetSelectionForControlBackspace(InputModifiers modifiers) { SelectionStart = CaretIndex; diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index 060d559d1a..cff3dab150 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -105,20 +105,17 @@ namespace Avalonia.Controls { if (control != null && control.IsVisible && control.GetVisualRoot() != null) { - if (s_popup == null) + if (s_popup != null) { - s_popup = new PopupRoot - { - Content = new ToolTip(), - }; - - ((ISetLogicalParent)s_popup).SetParent(control); + throw new AvaloniaInternalException("Previous ToolTip not disposed."); } var cp = MouseDevice.Instance?.GetPosition(control); var position = control.PointToScreen(cp ?? new Point(0, 0)) + new Vector(0, 22); - ((ToolTip)s_popup.Content).Content = GetTip(control); + s_popup = new PopupRoot(); + ((ISetLogicalParent)s_popup).SetParent(control); + s_popup.Content = new ToolTip { Content = GetTip(control) }; s_popup.Position = position; s_popup.Show(); @@ -148,9 +145,15 @@ namespace Avalonia.Controls if (control == s_current) { - if (s_popup != null && s_popup.IsVisible) + if (s_popup != null) { - s_popup.Hide(); + // Clear the ToolTip's Content in case it has control content: this will + // reset its visual parent allowing it to be used again. + ((ToolTip)s_popup.Content).Content = null; + + // Dispose of the popup. + s_popup.Dispose(); + s_popup = null; } s_show.OnNext(null); diff --git a/src/Avalonia.DotNetFrameworkRuntime/Avalonia.DotNetFrameworkRuntime.v2.ncrunchproject b/src/Avalonia.DotNetFrameworkRuntime/Avalonia.DotNetFrameworkRuntime.v2.ncrunchproject new file mode 100644 index 0000000000..30815b1937 --- /dev/null +++ b/src/Avalonia.DotNetFrameworkRuntime/Avalonia.DotNetFrameworkRuntime.v2.ncrunchproject @@ -0,0 +1,26 @@ + + true + 1000 + false + false + false + true + false + false + false + false + false + true + true + false + true + true + true + 60000 + + + + AutoDetect + STA + x86 + \ No newline at end of file diff --git a/src/Avalonia.SceneGraph/Media/PathMarkupParser.cs b/src/Avalonia.SceneGraph/Media/PathMarkupParser.cs index 3c0150e2d3..a9b4c21948 100644 --- a/src/Avalonia.SceneGraph/Media/PathMarkupParser.cs +++ b/src/Avalonia.SceneGraph/Media/PathMarkupParser.cs @@ -17,21 +17,13 @@ namespace Avalonia.Media private static readonly Dictionary Commands = new Dictionary { { 'F', Command.FillRule }, - { 'f', Command.FillRule }, { 'M', Command.Move }, - { 'm', Command.MoveRelative }, { 'L', Command.Line }, - { 'l', Command.LineRelative }, { 'H', Command.HorizontalLine }, - { 'h', Command.HorizontalLineRelative }, { 'V', Command.VerticalLine }, - { 'v', Command.VerticalLineRelative }, { 'C', Command.CubicBezierCurve }, - { 'c', Command.CubicBezierCurveRelative }, { 'A', Command.Arc }, - { 'a', Command.Arc }, { 'Z', Command.Close }, - { 'z', Command.Close }, }; private static readonly Dictionary FillRules = new Dictionary @@ -63,18 +55,12 @@ namespace Avalonia.Media None, FillRule, Move, - MoveRelative, Line, - LineRelative, HorizontalLine, - HorizontalLineRelative, VerticalLine, - VerticalLineRelative, CubicBezierCurve, - CubicBezierCurveRelative, Arc, Close, - Eof, } /// @@ -87,11 +73,11 @@ namespace Avalonia.Media using (StringReader reader = new StringReader(s)) { - Command lastCommand = Command.None; - Command command; + Command command = Command.None; Point point = new Point(); + bool relative = false; - while ((command = ReadCommand(reader, lastCommand)) != Command.Eof) + while (ReadCommand(reader, ref command, ref relative)) { switch (command) { @@ -100,72 +86,58 @@ namespace Avalonia.Media break; case Command.Move: - case Command.MoveRelative: if (openFigure) { _context.EndFigure(false); } - point = command == Command.Move ? - ReadPoint(reader) : - ReadRelativePoint(reader, point); - + point = ReadPoint(reader, point, relative); _context.BeginFigure(point, true); openFigure = true; break; case Command.Line: - point = ReadPoint(reader); - _context.LineTo(point); - break; - - case Command.LineRelative: - point = ReadRelativePoint(reader, point); + point = ReadPoint(reader, point, relative); _context.LineTo(point); break; case Command.HorizontalLine: - point = point.WithX(ReadDouble(reader)); - _context.LineTo(point); - break; + if (!relative) + { + point = point.WithX(ReadDouble(reader)); + } + else + { + point = new Point(point.X + ReadDouble(reader), point.Y); + } - case Command.HorizontalLineRelative: - point = new Point(point.X + ReadDouble(reader), point.Y); _context.LineTo(point); break; case Command.VerticalLine: - point = point.WithY(ReadDouble(reader)); - _context.LineTo(point); - break; + if (!relative) + { + point = point.WithY(ReadDouble(reader)); + } + else + { + point = new Point(point.X, point.Y + ReadDouble(reader)); + } - case Command.VerticalLineRelative: - point = new Point(point.X, point.Y + ReadDouble(reader)); _context.LineTo(point); break; case Command.CubicBezierCurve: { - Point point1 = ReadPoint(reader); - Point point2 = ReadPoint(reader); - point = ReadPoint(reader); + Point point1 = ReadPoint(reader, point, relative); + Point point2 = ReadPoint(reader, point, relative); + point = ReadPoint(reader, point, relative); _context.CubicBezierTo(point1, point2, point); break; } - case Command.CubicBezierCurveRelative: - { - Point point1 = ReadRelativePoint(reader, point); - Point point2 = ReadRelativePoint(reader, point); - _context.CubicBezierTo(point, point1, point2); - point = point2; - break; - } - case Command.Arc: { - //example: A10,10 0 0,0 10,20 - //format - size rotationAngle isLargeArcFlag sweepDirectionFlag endPoint Size size = ReadSize(reader); ReadSeparator(reader); double rotationAngle = ReadDouble(reader); @@ -173,7 +145,7 @@ namespace Avalonia.Media bool isLargeArc = ReadBool(reader); ReadSeparator(reader); SweepDirection sweepDirection = ReadBool(reader) ? SweepDirection.Clockwise : SweepDirection.CounterClockwise; - point = ReadPoint(reader); + point = ReadPoint(reader, point, relative); _context.ArcTo(point, size, rotationAngle, isLargeArc, sweepDirection); break; @@ -187,8 +159,6 @@ namespace Avalonia.Media default: throw new NotSupportedException("Unsupported command"); } - - lastCommand = command; } if (openFigure) @@ -198,7 +168,10 @@ namespace Avalonia.Media } } - private static Command ReadCommand(StringReader reader, Command lastCommand) + private static bool ReadCommand( + StringReader reader, + ref Command command, + ref bool relative) { ReadWhitespace(reader); @@ -206,19 +179,19 @@ namespace Avalonia.Media if (i == -1) { - return Command.Eof; + return false; } else { char c = (char)i; - Command command = Command.None; + Command next = Command.None; - if (!Commands.TryGetValue(c, out command)) + if (!Commands.TryGetValue(char.ToUpperInvariant(c), out next)) { if ((char.IsDigit(c) || c == '.' || c == '+' || c == '-') && - (lastCommand != Command.None)) + (command != Command.None)) { - return lastCommand; + return true; } else { @@ -226,8 +199,10 @@ namespace Avalonia.Media } } + command = next; + relative = char.IsLower(c); reader.Read(); - return command; + return true; } } @@ -297,12 +272,17 @@ namespace Avalonia.Media return double.Parse(b.ToString(), CultureInfo.InvariantCulture); } - private static Point ReadPoint(StringReader reader) + private static Point ReadPoint(StringReader reader, Point current, bool relative) { + if (!relative) + { + current = new Point(); + } + ReadWhitespace(reader); - double x = ReadDouble(reader); + double x = current.X + ReadDouble(reader); ReadSeparator(reader); - double y = ReadDouble(reader); + double y = current.Y + ReadDouble(reader); return new Point(x, y); } diff --git a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml index 2d1e833199..38786f54d3 100644 --- a/src/Avalonia.Themes.Default/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Default/Accents/BaseLight.xaml @@ -18,6 +18,8 @@ #99119EDA #66119EDA #33119EDA + Red + #10ff0000 2 0.5 diff --git a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj index f3f1714e2d..76ed678350 100644 --- a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj +++ b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj @@ -208,6 +208,11 @@ True + + + Designer + + + + @@ -28,8 +31,8 @@ - + diff --git a/src/Avalonia.Themes.Default/DropDown.xaml b/src/Avalonia.Themes.Default/DropDown.xaml index c33e4af4f4..5a3d44360c 100644 --- a/src/Avalonia.Themes.Default/DropDown.xaml +++ b/src/Avalonia.Themes.Default/DropDown.xaml @@ -10,10 +10,10 @@ BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> - + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia.Themes.Default/TextBox.xaml b/src/Avalonia.Themes.Default/TextBox.xaml index c68fab958e..8a5a41845b 100644 --- a/src/Avalonia.Themes.Default/TextBox.xaml +++ b/src/Avalonia.Themes.Default/TextBox.xaml @@ -27,10 +27,19 @@ - - + + + + + + + + + + + - + + @@ -57,7 +67,17 @@ - + + + \ No newline at end of file diff --git a/src/Gtk/Avalonia.Cairo/CairoPlatform.cs b/src/Gtk/Avalonia.Cairo/CairoPlatform.cs index 3b0d7d08da..6606ed4aef 100644 --- a/src/Gtk/Avalonia.Cairo/CairoPlatform.cs +++ b/src/Gtk/Avalonia.Cairo/CairoPlatform.cs @@ -53,13 +53,21 @@ namespace Avalonia.Cairo public IRenderTarget CreateRenderer(IPlatformHandle handle) { var window = handle as Gtk.Window; - if (window == null) - throw new NotSupportedException(string.Format( - "Don't know how to create a Cairo renderer from a '{0}' handle which isn't Gtk.Window", - handle.HandleDescriptor)); - - window.DoubleBuffered = true; - return new RenderTarget(window); + if (window != null) + { + window.DoubleBuffered = true; + return new RenderTarget(window); + } + var area = handle as Gtk.DrawingArea; + if (area != null) + { + area.DoubleBuffered = true; + return new RenderTarget(area); + } + + throw new NotSupportedException(string.Format( + "Don't know how to create a Cairo renderer from a '{0}' handle which isn't Gtk.Window or Gtk.DrawingArea", + handle.HandleDescriptor)); } public IRenderTargetBitmapImpl CreateRenderTargetBitmap(int width, int height) diff --git a/src/Gtk/Avalonia.Cairo/RenderTarget.cs b/src/Gtk/Avalonia.Cairo/RenderTarget.cs index cb7241f32e..d285986762 100644 --- a/src/Gtk/Avalonia.Cairo/RenderTarget.cs +++ b/src/Gtk/Avalonia.Cairo/RenderTarget.cs @@ -7,6 +7,7 @@ using Avalonia.Cairo.Media; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering; +using Gtk; using DrawingContext = Avalonia.Media.DrawingContext; namespace Avalonia.Cairo @@ -20,6 +21,7 @@ namespace Avalonia.Cairo { private readonly Surface _surface; private readonly Gtk.Window _window; + private readonly Gtk.DrawingArea _area; /// /// Initializes a new instance of the class. @@ -37,19 +39,28 @@ namespace Avalonia.Cairo _surface = surface; } + public RenderTarget(DrawingArea area) + { + _area = area; + } /// /// Creates a cairo surface that targets a platform-specific resource. /// /// A surface wrapped in an . - public DrawingContext CreateDrawingContext() + public DrawingContext CreateDrawingContext() => new DrawingContext(CreateMediaDrawingContext()); + + public IDrawingContextImpl CreateMediaDrawingContext() { - var ctx = _surface != null - ? new Media.DrawingContext(_surface) - : new Media.DrawingContext(_window.GdkWindow); - return new DrawingContext(ctx); + if (_window != null) + return new Media.DrawingContext(_window.GdkWindow); + if (_surface != null) + return new Media.DrawingContext(_surface); + if (_area != null) + return new Media.DrawingContext(_area.GdkWindow); + throw new InvalidOperationException("Unspecified render target"); } - + public void Dispose() => _surface?.Dispose(); } } diff --git a/src/Gtk/Avalonia.Gtk/Avalonia.Gtk.csproj b/src/Gtk/Avalonia.Gtk/Avalonia.Gtk.csproj index 2a47aa99fa..7e8c118196 100644 --- a/src/Gtk/Avalonia.Gtk/Avalonia.Gtk.csproj +++ b/src/Gtk/Avalonia.Gtk/Avalonia.Gtk.csproj @@ -46,6 +46,8 @@ + + @@ -54,8 +56,10 @@ + + @@ -67,6 +71,10 @@ {B09B78D8-9B26-48B0-9149-D64A2F120F3F} Avalonia.Base + + {7062ae20-5dcc-4442-9645-8195bdece63e} + Avalonia.Diagnostics + {62024B2D-53EB-4638-B26B-85EEAA54866E} Avalonia.Input diff --git a/src/Gtk/Avalonia.Gtk/EmbeddableImpl.cs b/src/Gtk/Avalonia.Gtk/EmbeddableImpl.cs new file mode 100644 index 0000000000..62b2513856 --- /dev/null +++ b/src/Gtk/Avalonia.Gtk/EmbeddableImpl.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Disposables; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Platform; +using Gdk; +using Gtk; +using Action = System.Action; +using WindowEdge = Avalonia.Controls.WindowEdge; + +namespace Avalonia.Gtk +{ + class EmbeddableImpl : WindowImplBase, IEmbeddableWindowImpl + { + public event Action LostFocus; + + public EmbeddableImpl(DrawingArea area) : base(area) + { + area.Events = EventMask.AllEventsMask; + area.SizeAllocated += Plug_SizeAllocated; + } + + public EmbeddableImpl() : this(new PlatformHandleAwareDrawingArea()) + { + } + + private void Plug_SizeAllocated(object o, SizeAllocatedArgs args) + { + Resized?.Invoke(new Size(args.Allocation.Width, args.Allocation.Height)); + } + + public override Size ClientSize + { + get { return new Size(Widget.Allocation.Width, Widget.Allocation.Height); } + set {} + } + + + //Stubs are needed for future GTK designer embedding support + public override void SetTitle(string title) + { + } + + public override IDisposable ShowDialog() => Disposable.Create(() => { }); + + public override void SetSystemDecorations(bool enabled) + { + } + + public override void SetIcon(IWindowIconImpl icon) + { + } + + public override void BeginMoveDrag() + { + } + + public override void BeginResizeDrag(WindowEdge edge) + { + } + + public override Point Position + { + get { return new Point(); } + set {} + } + } +} diff --git a/src/Gtk/Avalonia.Gtk/Embedding/GtkAvaloniaControlHost.cs b/src/Gtk/Avalonia.Gtk/Embedding/GtkAvaloniaControlHost.cs new file mode 100644 index 0000000000..5594a3c13f --- /dev/null +++ b/src/Gtk/Avalonia.Gtk/Embedding/GtkAvaloniaControlHost.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Controls.Embedding; +using Avalonia.Diagnostics; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Platform; +using Avalonia.VisualTree; +using Gdk; +using Gtk; + +namespace Avalonia.Gtk.Embedding +{ + public class GtkAvaloniaControlHost : DrawingArea, IPlatformHandle + { + private EmbeddableControlRoot _root; + + public GtkAvaloniaControlHost() + { + _root = new EmbeddableControlRoot(new EmbeddableImpl(this)); + _root.Prepare(); + if (_root.IsFocused) + Unfocus(); + _root.GotFocus += RootGotFocus; + CanFocus = true; + } + + void Unfocus() + { + var focused = (IVisual)FocusManager.Instance.Current; + if (focused == null) + return; + while (focused.VisualParent != null) + focused = focused.VisualParent; + + if (focused == _root) + KeyboardDevice.Instance.SetFocusedElement(null, NavigationMethod.Unspecified, InputModifiers.None); + } + + protected override bool OnFocusOutEvent(EventFocus evnt) + { + Unfocus(); + return false; + } + + private void RootGotFocus(object sender, RoutedEventArgs e) + { + this.HasFocus = true; + GdkWindow.Focus(0); + } + + private Control _content; + + public Control Content + { + get { return _content; } + set + { + _content = value; + if (_root != null) + { + _root.Content = value; + _root.Prepare(); + } + } + } + + IntPtr IPlatformHandle.Handle => PlatformHandleAwareWindow.GetNativeWindow(GdkWindow); + + string IPlatformHandle.HandleDescriptor => "HWND"; + } +} diff --git a/src/Gtk/Avalonia.Gtk/GtkPlatform.cs b/src/Gtk/Avalonia.Gtk/GtkPlatform.cs index c8246fc9f5..455aa67405 100644 --- a/src/Gtk/Avalonia.Gtk/GtkPlatform.cs +++ b/src/Gtk/Avalonia.Gtk/GtkPlatform.cs @@ -103,10 +103,7 @@ namespace Avalonia.Gtk return new WindowImpl(); } - public IWindowImpl CreateEmbeddableWindow() - { - throw new NotSupportedException(); - } + public IEmbeddableWindowImpl CreateEmbeddableWindow() => new EmbeddableImpl(); public IPopupImpl CreatePopup() { diff --git a/src/Gtk/Avalonia.Gtk/SystemDialogImpl.cs b/src/Gtk/Avalonia.Gtk/SystemDialogImpl.cs index a47a0a0ef8..ed92a25873 100644 --- a/src/Gtk/Avalonia.Gtk/SystemDialogImpl.cs +++ b/src/Gtk/Avalonia.Gtk/SystemDialogImpl.cs @@ -15,7 +15,7 @@ namespace Avalonia.Gtk public Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) { var tcs = new TaskCompletionSource(); - var dlg = new global::Gtk.FileChooserDialog(dialog.Title, ((WindowImpl)parent), + var dlg = new global::Gtk.FileChooserDialog(dialog.Title, ((WindowImplBase)parent).Widget.Toplevel as Window, dialog is OpenFileDialog ? FileChooserAction.Open : FileChooserAction.Save, @@ -57,7 +57,7 @@ namespace Avalonia.Gtk public Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) { var tcs = new TaskCompletionSource(); - var dlg = new global::Gtk.FileChooserDialog(dialog.Title, ((WindowImpl)parent), + var dlg = new global::Gtk.FileChooserDialog(dialog.Title, ((WindowImplBase)parent).Widget.Toplevel as Window, FileChooserAction.SelectFolder, "Cancel", ResponseType.Cancel, "Select Folder", ResponseType.Accept) diff --git a/src/Gtk/Avalonia.Gtk/WindowImpl.cs b/src/Gtk/Avalonia.Gtk/WindowImpl.cs index 26ad52135d..c1a69e8932 100644 --- a/src/Gtk/Avalonia.Gtk/WindowImpl.cs +++ b/src/Gtk/Avalonia.Gtk/WindowImpl.cs @@ -1,404 +1,118 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - using System; using System.Reactive.Disposables; -using System.Runtime.InteropServices; -using Gdk; -using Avalonia.Controls; -using Avalonia.Input.Raw; using Avalonia.Platform; -using Avalonia.Input; -using Avalonia.Threading; -using Action = System.Action; -using WindowEdge = Avalonia.Controls.WindowEdge; +using Gdk; namespace Avalonia.Gtk { using Gtk = global::Gtk; - - public class WindowImpl : Gtk.Window, IWindowImpl, IPlatformHandle + public class WindowImpl : WindowImplBase { - private IInputRoot _inputRoot; + private Gtk.Window _window; + private Gtk.Window Window => _window ?? (_window = (Gtk.Window) Widget); - private Size _lastClientSize; - private Gtk.IMContext _imContext; - private uint _lastKeyEventTimestamp; - - private static readonly Gdk.Cursor DefaultCursor = new Gdk.Cursor(CursorType.LeftPtr); - - public WindowImpl() - : base(Gtk.WindowType.Toplevel) + public WindowImpl(Gtk.WindowType type) : base(new PlatformHandleAwareWindow(type)) { - DefaultSize = new Gdk.Size(900, 480); Init(); } - public WindowImpl(Gtk.WindowType type) - : base(type) + public WindowImpl() + : base(new PlatformHandleAwareWindow(Gtk.WindowType.Toplevel) {DefaultSize = new Gdk.Size(900, 480)}) { Init(); } - private void Init() + void Init() { - Events = EventMask.PointerMotionMask | - EventMask.ButtonPressMask | - EventMask.ButtonReleaseMask; - _imContext = new Gtk.IMMulticontext(); - _imContext.Commit += ImContext_Commit; - DoubleBuffered = false; - Realize(); + Window.FocusActivated += OnFocusActivated; + Window.ConfigureEvent += OnConfigureEvent; _lastClientSize = ClientSize; } - - protected override void OnRealized () - { - base.OnRealized (); - _imContext.ClientWindow = this.GdkWindow; - } - - public Size ClientSize + private Size _lastClientSize; + void OnConfigureEvent(object o, Gtk.ConfigureEventArgs args) { - get - { - int width; - int height; - GetSize(out width, out height); - return new Size(width, height); - } - - set - { - Resize((int)value.Width, (int)value.Height); - } - } + var evnt = args.Event; + args.RetVal = true; + var newSize = new Size(evnt.Width, evnt.Height); - public Size MaxClientSize - { - get + if (newSize != _lastClientSize) { - // TODO: This should take into account things such as taskbar and window border - // thickness etc. - return new Size(Screen.Width, Screen.Height); + Resized(newSize); + _lastClientSize = newSize; } } - public Avalonia.Controls.WindowState WindowState + public override Size ClientSize { get { - switch (GdkWindow.State) - { - case Gdk.WindowState.Iconified: - return Controls.WindowState.Minimized; - case Gdk.WindowState.Maximized: - return Controls.WindowState.Maximized; - default: - return Controls.WindowState.Normal; - } + int width; + int height; + Window.GetSize(out width, out height); + return new Size(width, height); } set { - switch (value) - { - case Controls.WindowState.Minimized: - GdkWindow.Iconify(); - break; - case Controls.WindowState.Maximized: - GdkWindow.Maximize(); - break; - case Controls.WindowState.Normal: - GdkWindow.Deiconify(); - GdkWindow.Unmaximize(); - break; - } + Window.Resize((int)value.Width, (int)value.Height); } } - public double Scaling => 1; - - IPlatformHandle ITopLevelImpl.Handle => this; - - [DllImport("libgdk-win32-2.0-0.dll", CallingConvention = CallingConvention.Cdecl)] - extern static IntPtr gdk_win32_drawable_get_handle(IntPtr gdkWindow); - - [DllImport("libgtk-x11-2.0.so.0", CallingConvention = CallingConvention.Cdecl)] - extern static IntPtr gdk_x11_drawable_get_xid(IntPtr gdkWindow); - - [DllImport("libgdk-quartz-2.0-0.dylib", CallingConvention = CallingConvention.Cdecl)] - extern static IntPtr gdk_quartz_window_get_nswindow(IntPtr gdkWindow); - - IntPtr _nativeWindow; - - IntPtr GetNativeWindow() + public override void SetTitle(string title) { - IntPtr h = GdkWindow.Handle; - if (_nativeWindow != IntPtr.Zero) - return _nativeWindow; - //Try whatever backend that works - try - { - return _nativeWindow = gdk_quartz_window_get_nswindow(h); - } - catch - { - } - try - { - return _nativeWindow = gdk_x11_drawable_get_xid(h); - } - catch - { - } - return _nativeWindow = gdk_win32_drawable_get_handle(h); + Window.Title = title; } - - IntPtr IPlatformHandle.Handle => GetNativeWindow(); - public string HandleDescriptor => "HWND"; - - public Action Activated { get; set; } - - public Action Closed { get; set; } - - public Action Deactivated { get; set; } - - public Action Input { get; set; } - - public Action Paint { get; set; } - - public Action Resized { get; set; } - - public Action ScalingChanged { get; set; } - - public IPopupImpl CreatePopup() - { - return new PopupImpl(); - } - - public void Invalidate(Rect rect) - { - if (base.GdkWindow != null) - base.GdkWindow.InvalidateRect( - new Rectangle((int) rect.X, (int) rect.Y, (int) rect.Width, (int) rect.Height), true); - } - - public Point PointToClient(Point point) + void OnFocusActivated(object sender, EventArgs eventArgs) { - int x, y; - GdkWindow.GetDeskrelativeOrigin(out x, out y); - - return new Point(point.X - x, point.Y - y); - } - - public Point PointToScreen(Point point) - { - int x, y; - GdkWindow.GetDeskrelativeOrigin(out x, out y); - - return new Point(point.X + x, point.Y + y); - } - - public void SetInputRoot(IInputRoot inputRoot) - { - _inputRoot = inputRoot; - } - - public void SetTitle(string title) - { - Title = title; - } - - - public void SetCursor(IPlatformHandle cursor) - { - GdkWindow.Cursor = cursor != null ? new Gdk.Cursor(cursor.Handle) : DefaultCursor; + Activated(); } - public void BeginMoveDrag() + public override void BeginMoveDrag() { int x, y; ModifierType mod; - Screen.RootWindow.GetPointer(out x, out y, out mod); - BeginMoveDrag(1, x, y, 0); + Window.Screen.RootWindow.GetPointer(out x, out y, out mod); + Window.BeginMoveDrag(1, x, y, 0); } - public void BeginResizeDrag(WindowEdge edge) + public override void BeginResizeDrag(Controls.WindowEdge edge) { int x, y; ModifierType mod; - Screen.RootWindow.GetPointer(out x, out y, out mod); - BeginResizeDrag((Gdk.WindowEdge) (int) edge, 1, x, y, 0); + Window.Screen.RootWindow.GetPointer(out x, out y, out mod); + Window.BeginResizeDrag((Gdk.WindowEdge)(int)edge, 1, x, y, 0); } - public Point Position + public override Point Position { get { int x, y; - GetPosition(out x, out y); + Window.GetPosition(out x, out y); return new Point(x, y); } set { - Move((int)value.X, (int)value.Y); + Window.Move((int)value.X, (int)value.Y); } } - public IDisposable ShowDialog() + public override IDisposable ShowDialog() { - Modal = true; - Show(); + Window.Modal = true; + Window.Show(); return Disposable.Empty; } - public void SetSystemDecorations(bool enabled) => Decorated = enabled; - - void ITopLevelImpl.Activate() - { - Activate(); - } - - private static InputModifiers GetModifierKeys(ModifierType state) - { - var rv = InputModifiers.None; - if (state.HasFlag(ModifierType.ControlMask)) - rv |= InputModifiers.Control; - if (state.HasFlag(ModifierType.ShiftMask)) - rv |= InputModifiers.Shift; - if (state.HasFlag(ModifierType.Mod1Mask)) - rv |= InputModifiers.Control; - if(state.HasFlag(ModifierType.Button1Mask)) - rv |= InputModifiers.LeftMouseButton; - if (state.HasFlag(ModifierType.Button2Mask)) - rv |= InputModifiers.RightMouseButton; - if (state.HasFlag(ModifierType.Button3Mask)) - rv |= InputModifiers.MiddleMouseButton; - return rv; - } - - protected override bool OnButtonPressEvent(EventButton evnt) - { - - var e = new RawMouseEventArgs( - GtkMouseDevice.Instance, - evnt.Time, - _inputRoot, - evnt.Button == 1 - ? RawMouseEventType.LeftButtonDown - : evnt.Button == 3 ? RawMouseEventType.RightButtonDown : RawMouseEventType.MiddleButtonDown, - new Point(evnt.X, evnt.Y), GetModifierKeys(evnt.State)); - Input(e); - return true; - } - - protected override bool OnScrollEvent(EventScroll evnt) - { - double step = 1; - var delta = new Vector(); - if (evnt.Direction == ScrollDirection.Down) - delta = new Vector(0, -step); - else if (evnt.Direction == ScrollDirection.Up) - delta = new Vector(0, step); - else if (evnt.Direction == ScrollDirection.Right) - delta = new Vector(-step, 0); - if (evnt.Direction == ScrollDirection.Left) - delta = new Vector(step, 0); - var e = new RawMouseWheelEventArgs(GtkMouseDevice.Instance, evnt.Time, _inputRoot, new Point(evnt.X, evnt.Y), delta, GetModifierKeys(evnt.State)); - Input(e); - return base.OnScrollEvent(evnt); - } - - protected override bool OnButtonReleaseEvent(EventButton evnt) - { - var e = new RawMouseEventArgs( - GtkMouseDevice.Instance, - evnt.Time, - _inputRoot, - evnt.Button == 1 - ? RawMouseEventType.LeftButtonUp - : evnt.Button == 3 ? RawMouseEventType.RightButtonUp : RawMouseEventType.MiddleButtonUp, - new Point(evnt.X, evnt.Y), GetModifierKeys(evnt.State)); - Input(e); - return true; - } - - protected override bool OnConfigureEvent(EventConfigure evnt) - { - var newSize = new Size(evnt.Width, evnt.Height); - - if (newSize != _lastClientSize) - { - Resized(newSize); - _lastClientSize = newSize; - } - - return true; - } - - protected override void OnDestroyed() - { - Closed(); - } - - private bool ProcessKeyEvent(EventKey evnt) - { - _lastKeyEventTimestamp = evnt.Time; - if (_imContext.FilterKeypress(evnt)) - return true; - var e = new RawKeyEventArgs( - GtkKeyboardDevice.Instance, - evnt.Time, - evnt.Type == EventType.KeyPress ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp, - GtkKeyboardDevice.ConvertKey(evnt.Key), GetModifierKeys(evnt.State)); - Input(e); - return true; - } - - protected override bool OnKeyPressEvent(EventKey evnt) => ProcessKeyEvent(evnt); - - protected override bool OnKeyReleaseEvent(EventKey evnt) => ProcessKeyEvent(evnt); - - private void ImContext_Commit(object o, Gtk.CommitArgs args) - { - Input(new RawTextInputEventArgs(GtkKeyboardDevice.Instance, _lastKeyEventTimestamp, args.Str)); - } - - protected override bool OnExposeEvent(EventExpose evnt) - { - Paint(evnt.Area.ToAvalonia()); - return true; - } - - protected override void OnFocusActivated() - { - Activated(); - } - - protected override bool OnMotionNotifyEvent(EventMotion evnt) - { - var position = new Point(evnt.X, evnt.Y); - - GtkMouseDevice.Instance.SetClientPosition(position); - - var e = new RawMouseEventArgs( - GtkMouseDevice.Instance, - evnt.Time, - _inputRoot, - RawMouseEventType.Move, - position, GetModifierKeys(evnt.State)); - Input(e); - return true; - } + public override void SetSystemDecorations(bool enabled) => Window.Decorated = enabled; - public void SetIcon(IWindowIconImpl icon) + public override void SetIcon(IWindowIconImpl icon) { - Icon = ((IconImpl)icon).Pixbuf; + Window.Icon = ((IconImpl)icon).Pixbuf; } } -} +} \ No newline at end of file diff --git a/src/Gtk/Avalonia.Gtk/WindowImplBase.cs b/src/Gtk/Avalonia.Gtk/WindowImplBase.cs new file mode 100644 index 0000000000..c0020f5e6f --- /dev/null +++ b/src/Gtk/Avalonia.Gtk/WindowImplBase.cs @@ -0,0 +1,310 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Reactive.Disposables; +using System.Runtime.InteropServices; +using Gdk; +using Avalonia.Controls; +using Avalonia.Input.Raw; +using Avalonia.Platform; +using Avalonia.Input; +using Avalonia.Threading; +using Action = System.Action; +using WindowEdge = Avalonia.Controls.WindowEdge; + +namespace Avalonia.Gtk +{ + using Gtk = global::Gtk; + + public abstract class WindowImplBase : IWindowImpl + { + private IInputRoot _inputRoot; + protected Gtk.Widget _window; + public Gtk.Widget Widget => _window; + + + private Gtk.IMContext _imContext; + + private uint _lastKeyEventTimestamp; + + private static readonly Gdk.Cursor DefaultCursor = new Gdk.Cursor(CursorType.LeftPtr); + + protected WindowImplBase(Gtk.Widget window) + { + _window = window; + Init(); + } + + void Init() + { + Handle = _window as IPlatformHandle; + _window.Events = EventMask.AllEventsMask; + _imContext = new Gtk.IMMulticontext(); + _imContext.Commit += ImContext_Commit; + _window.Realized += OnRealized; + _window.DoubleBuffered = false; + _window.Realize(); + _window.ButtonPressEvent += OnButtonPressEvent; + _window.ButtonReleaseEvent += OnButtonReleaseEvent; + _window.ScrollEvent += OnScrollEvent; + _window.Destroyed += OnDestroyed; + _window.KeyPressEvent += OnKeyPressEvent; + _window.KeyReleaseEvent += OnKeyReleaseEvent; + _window.ExposeEvent += OnExposeEvent; + _window.MotionNotifyEvent += OnMotionNotifyEvent; + + } + + public IPlatformHandle Handle { get; private set; } + + void OnRealized (object sender, EventArgs eventArgs) + { + _imContext.ClientWindow = _window.GdkWindow; + } + + public abstract Size ClientSize { get; set; } + + + public Size MaxClientSize + { + get + { + // TODO: This should take into account things such as taskbar and window border + // thickness etc. + return new Size(_window.Screen.Width, _window.Screen.Height); + } + } + + public Avalonia.Controls.WindowState WindowState + { + get + { + switch (_window.GdkWindow.State) + { + case Gdk.WindowState.Iconified: + return Controls.WindowState.Minimized; + case Gdk.WindowState.Maximized: + return Controls.WindowState.Maximized; + default: + return Controls.WindowState.Normal; + } + } + + set + { + switch (value) + { + case Controls.WindowState.Minimized: + _window.GdkWindow.Iconify(); + break; + case Controls.WindowState.Maximized: + _window.GdkWindow.Maximize(); + break; + case Controls.WindowState.Normal: + _window.GdkWindow.Deiconify(); + _window.GdkWindow.Unmaximize(); + break; + } + } + } + + public double Scaling => 1; + + public Action Activated { get; set; } + + public Action Closed { get; set; } + + public Action Deactivated { get; set; } + + public Action Input { get; set; } + + public Action Paint { get; set; } + + public Action Resized { get; set; } + + public Action ScalingChanged { get; set; } + + public IPopupImpl CreatePopup() + { + return new PopupImpl(); + } + + public void Invalidate(Rect rect) + { + if (_window.GdkWindow != null) + _window.GdkWindow.InvalidateRect( + new Rectangle((int) rect.X, (int) rect.Y, (int) rect.Width, (int) rect.Height), true); + } + + public Point PointToClient(Point point) + { + int x, y; + _window.GdkWindow.GetDeskrelativeOrigin(out x, out y); + + return new Point(point.X - x, point.Y - y); + } + + public Point PointToScreen(Point point) + { + int x, y; + _window.GdkWindow.GetDeskrelativeOrigin(out x, out y); + return new Point(point.X + x, point.Y + y); + } + + public void SetInputRoot(IInputRoot inputRoot) + { + _inputRoot = inputRoot; + } + + public abstract void SetTitle(string title); + public abstract IDisposable ShowDialog(); + public abstract void SetSystemDecorations(bool enabled); + public abstract void SetIcon(IWindowIconImpl icon); + + + public void SetCursor(IPlatformHandle cursor) + { + _window.GdkWindow.Cursor = cursor != null ? new Gdk.Cursor(cursor.Handle) : DefaultCursor; + } + + public void Show() => _window.Show(); + + public void Hide() => _window.Hide(); + public abstract void BeginMoveDrag(); + public abstract void BeginResizeDrag(WindowEdge edge); + public abstract Point Position { get; set; } + + void ITopLevelImpl.Activate() + { + _window.Activate(); + } + + private static InputModifiers GetModifierKeys(ModifierType state) + { + var rv = InputModifiers.None; + if (state.HasFlag(ModifierType.ControlMask)) + rv |= InputModifiers.Control; + if (state.HasFlag(ModifierType.ShiftMask)) + rv |= InputModifiers.Shift; + if (state.HasFlag(ModifierType.Mod1Mask)) + rv |= InputModifiers.Control; + if(state.HasFlag(ModifierType.Button1Mask)) + rv |= InputModifiers.LeftMouseButton; + if (state.HasFlag(ModifierType.Button2Mask)) + rv |= InputModifiers.RightMouseButton; + if (state.HasFlag(ModifierType.Button3Mask)) + rv |= InputModifiers.MiddleMouseButton; + return rv; + } + + void OnButtonPressEvent(object o, Gtk.ButtonPressEventArgs args) + { + var evnt = args.Event; + var e = new RawMouseEventArgs( + GtkMouseDevice.Instance, + evnt.Time, + _inputRoot, + evnt.Button == 1 + ? RawMouseEventType.LeftButtonDown + : evnt.Button == 3 ? RawMouseEventType.RightButtonDown : RawMouseEventType.MiddleButtonDown, + new Point(evnt.X, evnt.Y), GetModifierKeys(evnt.State)); + Input(e); + } + + void OnScrollEvent(object o, Gtk.ScrollEventArgs args) + { + var evnt = args.Event; + double step = 1; + var delta = new Vector(); + if (evnt.Direction == ScrollDirection.Down) + delta = new Vector(0, -step); + else if (evnt.Direction == ScrollDirection.Up) + delta = new Vector(0, step); + else if (evnt.Direction == ScrollDirection.Right) + delta = new Vector(-step, 0); + if (evnt.Direction == ScrollDirection.Left) + delta = new Vector(step, 0); + var e = new RawMouseWheelEventArgs(GtkMouseDevice.Instance, evnt.Time, _inputRoot, new Point(evnt.X, evnt.Y), delta, GetModifierKeys(evnt.State)); + Input(e); + } + + protected void OnButtonReleaseEvent(object o, Gtk.ButtonReleaseEventArgs args) + { + var evnt = args.Event; + var e = new RawMouseEventArgs( + GtkMouseDevice.Instance, + evnt.Time, + _inputRoot, + evnt.Button == 1 + ? RawMouseEventType.LeftButtonUp + : evnt.Button == 3 ? RawMouseEventType.RightButtonUp : RawMouseEventType.MiddleButtonUp, + new Point(evnt.X, evnt.Y), GetModifierKeys(evnt.State)); + Input(e); + } + + void OnDestroyed(object sender, EventArgs eventArgs) + { + Closed(); + } + + private void ProcessKeyEvent(EventKey evnt) + { + + _lastKeyEventTimestamp = evnt.Time; + if (_imContext.FilterKeypress(evnt)) + return; + var e = new RawKeyEventArgs( + GtkKeyboardDevice.Instance, + evnt.Time, + evnt.Type == EventType.KeyPress ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp, + GtkKeyboardDevice.ConvertKey(evnt.Key), GetModifierKeys(evnt.State)); + Input(e); + } + + void OnKeyPressEvent(object o, Gtk.KeyPressEventArgs args) + { + args.RetVal = true; + ProcessKeyEvent(args.Event); + } + + void OnKeyReleaseEvent(object o, Gtk.KeyReleaseEventArgs args) + { + args.RetVal = true; + ProcessKeyEvent(args.Event); + } + + private void ImContext_Commit(object o, Gtk.CommitArgs args) + { + Input(new RawTextInputEventArgs(GtkKeyboardDevice.Instance, _lastKeyEventTimestamp, args.Str)); + } + + void OnExposeEvent(object o, Gtk.ExposeEventArgs args) + { + Paint(args.Event.Area.ToAvalonia()); + args.RetVal = true; + } + + void OnMotionNotifyEvent(object o, Gtk.MotionNotifyEventArgs args) + { + var evnt = args.Event; + var position = new Point(evnt.X, evnt.Y); + + GtkMouseDevice.Instance.SetClientPosition(position); + + var e = new RawMouseEventArgs( + GtkMouseDevice.Instance, + evnt.Time, + _inputRoot, + RawMouseEventType.Move, + position, GetModifierKeys(evnt.State)); + Input(e); + args.RetVal = true; + } + + public void Dispose() + { + _window.Dispose(); + } + } +} diff --git a/src/Gtk/Avalonia.Gtk/Windows.cs b/src/Gtk/Avalonia.Gtk/Windows.cs new file mode 100644 index 0000000000..fe325f4114 --- /dev/null +++ b/src/Gtk/Avalonia.Gtk/Windows.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Platform; +using Gdk; +using Gtk; +using Window = Gtk.Window; +using WindowType = Gtk.WindowType; + +namespace Avalonia.Gtk +{ + class PlatformHandleAwareWindow : Window, IPlatformHandle + { + public PlatformHandleAwareWindow(WindowType type) : base(type) + { + Events = EventMask.AllEventsMask; + } + + IntPtr IPlatformHandle.Handle => GetNativeWindow(); + public string HandleDescriptor => "HWND"; + + + [DllImport("libgdk-win32-2.0-0.dll", CallingConvention = CallingConvention.Cdecl)] + static extern IntPtr gdk_win32_drawable_get_handle(IntPtr gdkWindow); + + [DllImport("libgtk-x11-2.0.so.0", CallingConvention = CallingConvention.Cdecl)] + static extern IntPtr gdk_x11_drawable_get_xid(IntPtr gdkWindow); + + [DllImport("libgdk-quartz-2.0-0.dylib", CallingConvention = CallingConvention.Cdecl)] + static extern IntPtr gdk_quartz_window_get_nswindow(IntPtr gdkWindow); + + IntPtr _nativeWindow; + + IntPtr GetNativeWindow() + { + if (_nativeWindow != IntPtr.Zero) + return _nativeWindow; + return _nativeWindow = GetNativeWindow(GdkWindow); + } + + public static IntPtr GetNativeWindow(Gdk.Window window) + { + IntPtr h = window.Handle; + + //Try whatever backend that works + try + { + return gdk_quartz_window_get_nswindow(h); + } + catch + { + } + try + { + return gdk_x11_drawable_get_xid(h); + } + catch + { + } + return gdk_win32_drawable_get_handle(h); + } + + protected override bool OnConfigureEvent(EventConfigure evnt) + { + base.OnConfigureEvent(evnt); + return false; + } + } + + class PlatformHandleAwareDrawingArea : DrawingArea, IPlatformHandle + { + + + + IntPtr IPlatformHandle.Handle => GetNativeWindow(); + public string HandleDescriptor => "HWND"; + IntPtr _nativeWindow; + + IntPtr GetNativeWindow() + { + + if (_nativeWindow != IntPtr.Zero) + return _nativeWindow; + Realize(); + return _nativeWindow = PlatformHandleAwareWindow.GetNativeWindow(GdkWindow); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs b/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs index ec60695374..086257f24c 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs @@ -27,10 +27,12 @@ namespace Avalonia.Markup.Xaml.Data /// Initializes a new instance of the class. /// /// The binding path. - public Binding(string path) + /// The binding mode. + public Binding(string path, BindingMode mode = BindingMode.Default) : this() { Path = path; + Mode = mode; } /// @@ -78,21 +80,18 @@ namespace Avalonia.Markup.Xaml.Data /// public object Source { get; set; } - /// - /// Gets or sets a value indicating whether the property should be validated. - /// - public bool EnableValidation { get; set; } - /// public InstancedBinding Initiate( IAvaloniaObject target, AvaloniaProperty targetProperty, - object anchor = null) + object anchor = null, + bool enableDataValidation = false) { Contract.Requires(target != null); var pathInfo = ParsePath(Path); ValidateState(pathInfo); + enableDataValidation = enableDataValidation && Priority == BindingPriority.LocalValue; ExpressionObserver observer; @@ -105,7 +104,7 @@ namespace Avalonia.Markup.Xaml.Data } else if (Source != null) { - observer = CreateSourceObserver(Source, pathInfo.Path); + observer = CreateSourceObserver(Source, pathInfo.Path, enableDataValidation); } else if (RelativeSource == null || RelativeSource.Mode == RelativeSourceMode.DataContext) { @@ -113,7 +112,8 @@ namespace Avalonia.Markup.Xaml.Data target, pathInfo.Path, targetProperty == Control.DataContextProperty, - anchor); + anchor, + enableDataValidation); } else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent) { @@ -135,7 +135,7 @@ namespace Avalonia.Markup.Xaml.Data fallback = null; } - var subject = new ExpressionSubject( + var subject = new BindingExpression( observer, targetProperty?.PropertyType ?? typeof(object), fallback, @@ -197,7 +197,8 @@ namespace Avalonia.Markup.Xaml.Data IAvaloniaObject target, string path, bool targetIsDataContext, - object anchor) + object anchor, + bool enableDataValidation) { Contract.Requires(target != null); @@ -220,19 +221,16 @@ namespace Avalonia.Markup.Xaml.Data () => target.GetValue(Control.DataContextProperty), path, update, - EnableValidation); + enableDataValidation); return result; } else { return new ExpressionObserver( - target.GetObservable(Visual.VisualParentProperty) - .OfType() - .Select(x => x.GetObservable(Control.DataContextProperty)) - .Switch(), + GetParentDataContext(target), path, - EnableValidation); + enableDataValidation); } } @@ -240,18 +238,23 @@ namespace Avalonia.Markup.Xaml.Data { Contract.Requires(target != null); + var description = $"#{elementName}.{path}"; var result = new ExpressionObserver( ControlLocator.Track(target, elementName), path, - EnableValidation); + false, + description); return result; } - private ExpressionObserver CreateSourceObserver(object source, string path) + private ExpressionObserver CreateSourceObserver( + object source, + string path, + bool enabledDataValidation) { Contract.Requires(source != null); - return new ExpressionObserver(source, path, EnableValidation); + return new ExpressionObserver(source, path, enabledDataValidation); } private ExpressionObserver CreateTemplatedParentObserver( @@ -272,6 +275,22 @@ namespace Avalonia.Markup.Xaml.Data return result; } + private IObservable GetParentDataContext(IAvaloniaObject target) + { + // The DataContext is based on the visual parent and not the logical parent: this may + // seem unintuitive considering the fact that property inheritance works on the logical + // tree, but consider a ContentControl with a ContentPresenter. The ContentControl's + // Content property is bound to a value which becomes the ContentPresenter's + // DataContext - it is from this that the child hosted by the ContentPresenter needs to + // inherit its DataContext. + return target.GetObservable(Visual.VisualParentProperty) + .Select(x => + { + return (x as IAvaloniaObject)?.GetObservable(Control.DataContextProperty) ?? + Observable.Return((object)null); + }).Switch(); + } + private class PathInfo { public string Path { get; set; } diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup.Xaml/Data/MultiBinding.cs index 91a502a44f..69190be220 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Data/MultiBinding.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Data/MultiBinding.cs @@ -53,7 +53,8 @@ namespace Avalonia.Markup.Xaml.Data public InstancedBinding Initiate( IAvaloniaObject target, AvaloniaProperty targetProperty, - object anchor = null) + object anchor = null, + bool enableDataValidation = false) { if (Converter == null) { diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/StyleResourceBinding.cs b/src/Markup/Avalonia.Markup.Xaml/Data/StyleResourceBinding.cs index 442f9199e8..c538c0768e 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Data/StyleResourceBinding.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Data/StyleResourceBinding.cs @@ -37,7 +37,8 @@ namespace Avalonia.Markup.Xaml.Data public InstancedBinding Initiate( IAvaloniaObject target, AvaloniaProperty targetProperty, - object anchor = null) + object anchor = null, + bool enableDataValidation = false) { if (Name == "Red") { diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs index 70d3f7d161..b131f07fd4 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs @@ -29,7 +29,6 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions Mode = Mode, Path = Path, Priority = Priority, - EnableValidation = EnableValidation, }; } @@ -41,6 +40,5 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions public string Path { get; set; } public BindingPriority Priority { get; set; } = BindingPriority.LocalValue; public object Source { get; set; } - public bool EnableValidation { get; set; } } } \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs index be4287605c..5e412633d2 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs @@ -5,14 +5,13 @@ using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Markup.Data; using System; +using System.Reactive.Linq; namespace Avalonia.Markup.Xaml.Templates { public class MemberSelector : IMemberSelector { - private ExpressionNode _expressionNode; private string _memberName; - private ExpressionNode _memberValueNode; public string MemberName { @@ -22,8 +21,6 @@ namespace Avalonia.Markup.Xaml.Templates if (_memberName != value) { _memberName = value; - _expressionNode = null; - _memberValueNode = null; } } } @@ -35,34 +32,11 @@ namespace Avalonia.Markup.Xaml.Templates return o; } - if (_expressionNode == null) - { - _expressionNode = ExpressionNodeBuilder.Build(MemberName); - - _memberValueNode = _expressionNode; - - while (_memberValueNode.Next != null) - { - _memberValueNode = _memberValueNode.Next; - } - } - - _expressionNode.Target = new WeakReference(o); - - object result = _memberValueNode.CurrentValue.Target; - - _expressionNode.Target = null; - - if (result == AvaloniaProperty.UnsetValue) - { - return null; - } - else if (result is BindingError) - { - return null; - } + var expression = new ExpressionObserver(o, MemberName); + object result = AvaloniaProperty.UnsetValue; - return result; + expression.Subscribe(x => result = x); + return (result == AvaloniaProperty.UnsetValue || result is BindingNotification) ? null : result; } } } \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj index 49a5a4a681..738f381f4e 100644 --- a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj +++ b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj @@ -41,14 +41,17 @@ Properties\SharedAssemblyInfo.cs + + - + + - + @@ -59,8 +62,12 @@ + + + + - + diff --git a/src/Markup/Avalonia.Markup/Data/BindingExpression.cs b/src/Markup/Avalonia.Markup/Data/BindingExpression.cs new file mode 100644 index 0000000000..0f4c091bff --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/BindingExpression.cs @@ -0,0 +1,305 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Globalization; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using Avalonia.Data; +using Avalonia.Logging; +using Avalonia.Utilities; + +namespace Avalonia.Markup.Data +{ + /// + /// Binds to an expression on an object using a type value converter to convert the values + /// that are send and received. + /// + public class BindingExpression : ISubject, IDescription + { + private readonly ExpressionObserver _inner; + private readonly Type _targetType; + private readonly object _fallbackValue; + private readonly BindingPriority _priority; + private readonly Subject _errors = new Subject(); + + /// + /// Initializes a new instance of the class. + /// + /// The . + /// The type to convert the value to. + public BindingExpression(ExpressionObserver inner, Type targetType) + : this(inner, targetType, DefaultValueConverter.Instance) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The . + /// The type to convert the value to. + /// The value converter to use. + /// + /// A parameter to pass to . + /// + /// The binding priority. + public BindingExpression( + ExpressionObserver inner, + Type targetType, + IValueConverter converter, + object converterParameter = null, + BindingPriority priority = BindingPriority.LocalValue) + : this(inner, targetType, AvaloniaProperty.UnsetValue, converter, converterParameter, priority) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The . + /// The type to convert the value to. + /// + /// The value to use when the binding is unable to produce a value. + /// + /// The value converter to use. + /// + /// A parameter to pass to . + /// + /// The binding priority. + public BindingExpression( + ExpressionObserver inner, + Type targetType, + object fallbackValue, + IValueConverter converter, + object converterParameter = null, + BindingPriority priority = BindingPriority.LocalValue) + { + Contract.Requires(inner != null); + Contract.Requires(targetType != null); + Contract.Requires(converter != null); + + _inner = inner; + _targetType = targetType; + Converter = converter; + ConverterParameter = converterParameter; + _fallbackValue = fallbackValue; + _priority = priority; + } + + /// + /// Gets the converter to use on the expression. + /// + public IValueConverter Converter { get; } + + /// + /// Gets a parameter to pass to . + /// + public object ConverterParameter { get; } + + /// + string IDescription.Description => _inner.Expression; + + /// + public void OnCompleted() + { + } + + /// + public void OnError(Exception error) + { + } + + /// + public void OnNext(object value) + { + using (_inner.Subscribe(_ => { })) + { + var type = _inner.ResultType; + + if (type != null) + { + var converted = Converter.ConvertBack( + value, + type, + ConverterParameter, + CultureInfo.CurrentUICulture); + + if (converted == AvaloniaProperty.UnsetValue) + { + converted = TypeUtilities.Default(type); + _inner.SetValue(converted, _priority); + } + else if (converted is BindingNotification) + { + var notification = converted as BindingNotification; + + if (notification.ErrorType == BindingErrorType.None) + { + throw new AvaloniaInternalException( + "IValueConverter should not return non-errored BindingNotification."); + } + + _errors.OnNext(notification); + + if (_fallbackValue != AvaloniaProperty.UnsetValue) + { + if (TypeUtilities.TryConvert( + type, + _fallbackValue, + CultureInfo.InvariantCulture, + out converted)) + { + _inner.SetValue(converted, _priority); + } + else + { + Logger.Error( + LogArea.Binding, + this, + "Could not convert FallbackValue {FallbackValue} to {Type}", + _fallbackValue, + type); + } + } + } + else + { + _inner.SetValue(converted, _priority); + } + } + } + } + + /// + public IDisposable Subscribe(IObserver observer) + { + return _inner.Select(ConvertValue).Merge(_errors).Subscribe(observer); + } + + private object ConvertValue(object value) + { + var notification = value as BindingNotification; + + if (notification == null) + { + var converted = Converter.Convert( + value, + _targetType, + ConverterParameter, + CultureInfo.CurrentUICulture); + + notification = converted as BindingNotification; + + if (notification?.ErrorType == BindingErrorType.None) + { + converted = notification.Value; + } + + if (_fallbackValue != AvaloniaProperty.UnsetValue && + (converted == AvaloniaProperty.UnsetValue || converted is BindingNotification)) + { + var fallback = ConvertFallback(); + converted = Merge(converted, fallback); + } + + return converted; + } + else + { + return ConvertValue(notification); + } + } + + private BindingNotification ConvertValue(BindingNotification notification) + { + if (notification.HasValue) + { + var converted = ConvertValue(notification.Value); + notification = Merge(notification, converted); + } + else if (_fallbackValue != AvaloniaProperty.UnsetValue) + { + var fallback = ConvertFallback(); + notification = Merge(notification, fallback); + } + + return notification; + } + + private BindingNotification ConvertFallback() + { + object converted; + + if (_fallbackValue == AvaloniaProperty.UnsetValue) + { + throw new AvaloniaInternalException("Cannot call ConvertFallback with no fallback value"); + } + + if (TypeUtilities.TryConvert( + _targetType, + _fallbackValue, + CultureInfo.InvariantCulture, + out converted)) + { + return new BindingNotification(converted); + } + else + { + return new BindingNotification( + new InvalidCastException( + $"Could not convert FallbackValue '{_fallbackValue}' to '{_targetType}'"), + BindingErrorType.Error); + } + } + + private static BindingNotification Merge(object a, BindingNotification b) + { + var an = a as BindingNotification; + + if (an != null) + { + Merge(an, b); + return an; + } + else + { + return b; + } + } + + private static BindingNotification Merge(BindingNotification a, object b) + { + var bn = b as BindingNotification; + + if (bn != null) + { + Merge(a, bn); + } + else + { + a.SetValue(b); + } + + return a; + } + + private static BindingNotification Merge(BindingNotification a, BindingNotification b) + { + if (b.HasValue) + { + a.SetValue(b.Value); + } + else + { + a.ClearValue(); + } + + if (b.Error != null) + { + a.AddError(b.Error, b.ErrorType); + } + + return a; + } + } +} diff --git a/src/Markup/Avalonia.Markup/Data/EmptyExpressionNode.cs b/src/Markup/Avalonia.Markup/Data/EmptyExpressionNode.cs new file mode 100644 index 0000000000..02ecd817da --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/EmptyExpressionNode.cs @@ -0,0 +1,18 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Reactive.Linq; + +namespace Avalonia.Markup.Data +{ + internal class EmptyExpressionNode : ExpressionNode + { + public override string Description => "."; + + protected override IObservable StartListeningCore(WeakReference reference) + { + return Observable.Return(reference.Target); + } + } +} diff --git a/src/Markup/Avalonia.Markup/Data/ExpressionNode.cs b/src/Markup/Avalonia.Markup/Data/ExpressionNode.cs index 90a654d9e6..b0957c7187 100644 --- a/src/Markup/Avalonia.Markup/Data/ExpressionNode.cs +++ b/src/Markup/Avalonia.Markup/Data/ExpressionNode.cs @@ -2,124 +2,188 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Reactive.Disposables; +using System.Reactive.Linq; using System.Reactive.Subjects; using Avalonia.Data; namespace Avalonia.Markup.Data { - internal abstract class ExpressionNode : IObservable + internal abstract class ExpressionNode : ISubject { protected static readonly WeakReference UnsetReference = new WeakReference(AvaloniaProperty.UnsetValue); - private WeakReference _target; - - private Subject _subject; - - private WeakReference _value = UnsetReference; + private WeakReference _target = UnsetReference; + private IDisposable _valueSubscription; + private IObserver _observer; + private IDisposable _valuePluginSubscription; + public abstract string Description { get; } public ExpressionNode Next { get; set; } public WeakReference Target { - get - { - return _target; - } + get { return _target; } set { - var newInstance = value?.Target; - var oldInstance = _target?.Target; + Contract.Requires(value != null); - if (!object.Equals(oldInstance, newInstance)) - { - if (oldInstance != null) - { - Unsubscribe(oldInstance); - } + var oldTarget = _target?.Target; + var newTarget = value.Target; + var running = _valueSubscription != null; + if (!ReferenceEquals(oldTarget, newTarget)) + { + _valueSubscription?.Dispose(); + _valueSubscription = null; + _valuePluginSubscription?.Dispose(); _target = value; - if (newInstance != null) + if (running) { - SubscribeAndUpdate(_target); - } - else - { - CurrentValue = UnsetReference; - } - - if (Next != null) - { - Next.Target = _value; + _valueSubscription = StartListening(); } } } } - public WeakReference CurrentValue + public IDisposable Subscribe(IObserver observer) { - get + if (_observer != null) { - return _value; + throw new AvaloniaInternalException("ExpressionNode can only be subscribed once."); } - set + _observer = observer; + var nextSubscription = Next?.Subscribe(this); + _valueSubscription = StartListening(); + + return Disposable.Create(() => { - _value = value; + _valueSubscription?.Dispose(); + _valueSubscription = null; + _valuePluginSubscription?.Dispose(); + _valuePluginSubscription = null; + nextSubscription?.Dispose(); + _observer = null; + }); + } - if (Next != null) - { - Next.Target = value; - } + void IObserver.OnCompleted() + { + throw new AvaloniaInternalException("ExpressionNode.OnCompleted should not be called."); + } - _subject?.OnNext(value.Target); - } + void IObserver.OnError(Exception error) + { + throw new AvaloniaInternalException("ExpressionNode.OnError should not be called."); } - public virtual bool SetValue(object value, BindingPriority priority) + void IObserver.OnNext(object value) { - return Next?.SetValue(value, priority) ?? false; + NextValueChanged(value); } - public virtual IDisposable Subscribe(IObserver observer) + protected virtual IObservable StartListeningCore(WeakReference reference) { - if (Next != null) + return Observable.Return(reference.Target); + } + + protected virtual void NextValueChanged(object value) + { + var bindingBroken = BindingNotification.ExtractError(value) as MarkupBindingChainNullException; + bindingBroken?.AddNode(Description); + _observer.OnNext(value); + } + + private IDisposable StartListening() + { + var target = _target.Target; + IObservable source; + + if (target == null) + { + source = Observable.Return(TargetNullNotification()); + } + else if (target == AvaloniaProperty.UnsetValue) { - return Next.Subscribe(observer); + source = Observable.Empty(); } else { - if (_subject == null) - { - _subject = new Subject(); - } - - observer.OnNext(CurrentValue.Target); - return _subject.Subscribe(observer); + source = StartListeningCore(_target); } - } - protected virtual void SubscribeAndUpdate(WeakReference reference) - { - CurrentValue = reference; + return source.Subscribe(TargetValueChanged); } - protected virtual void SendValidationStatus(IValidationStatus status) + private void TargetValueChanged(object value) { - //Even if elements only bound to sub-values, send validation changes along so they will be surfaced to the UI level. - if (_subject != null) + var notification = value as BindingNotification; + + if (notification == null) { - _subject.OnNext(status); + if (!HandleSpecialValue(value)) + { + if (Next != null) + { + Next.Target = new WeakReference(value); + } + else + { + _observer.OnNext(value); + } + } } else { - Next?.SendValidationStatus(status); + if (notification.Error != null) + { + _observer.OnNext(notification); + } + else if (notification.HasValue) + { + if (!HandleSpecialValue(notification.Value)) + { + if (Next != null) + { + Next.Target = new WeakReference(notification.Value); + } + else + { + _observer.OnNext(value); + } + } + } + } + } + + private bool HandleSpecialValue(object value) + { + if (_valuePluginSubscription == null) + { + var reference = new WeakReference(value); + + foreach (var plugin in ExpressionObserver.ValueHandlers) + { + if (plugin.Match(reference)) + { + _valuePluginSubscription = plugin.Start(reference)?.Subscribe(TargetValueChanged); + return true; + } + } } + + return false; } - protected virtual void Unsubscribe(object target) + private BindingNotification TargetNullNotification() { + return new BindingNotification( + new MarkupBindingChainNullException(), + BindingErrorType.Error, + AvaloniaProperty.UnsetValue); } } } diff --git a/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs b/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs index 0b5fbc6d7d..819949b7b9 100644 --- a/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs +++ b/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; +using System.Reactive.Subjects; using Avalonia.Data; using Avalonia.Markup.Data.Plugins; @@ -31,41 +32,57 @@ namespace Avalonia.Markup.Data /// An ordered collection of validation checker plugins that can be used to customize /// the validation of view model and model data. /// - public static readonly IList ValidationCheckers = - new List + public static readonly IList DataValidators = + new List { + new DataAnnotationsValidationPlugin(), new IndeiValidationPlugin(), + new ExceptionValidationPlugin(), }; - private readonly WeakReference _root; - private readonly Func _rootGetter; - private readonly IObservable _rootObservable; - private readonly IObservable _update; - private IDisposable _rootObserverSubscription; - private IDisposable _updateSubscription; - private int _count; + /// + /// An ordered collection of value handlers that can be used to customize the handling + /// of certain values. + /// + public static readonly IList ValueHandlers = + new List + { + new TaskValuePlugin(), + new ObservableValuePlugin(), + }; + + private static readonly object UninitializedValue = new object(); private readonly ExpressionNode _node; - private bool _enableValidation; + private readonly Subject _finished; + private readonly object _root; + private IObservable _result; /// /// Initializes a new instance of the class. /// /// The root object. /// The expression. - /// Whether property validation should be enabled. - public ExpressionObserver(object root, string expression, bool enableValidation = false) + /// Whether data validation should be enabled. + /// + /// A description of the expression. If null, will be used. + /// + public ExpressionObserver( + object root, + string expression, + bool enableDataValidation = false, + string description = null) { Contract.Requires(expression != null); - _root = new WeakReference(root); - _enableValidation = enableValidation; - - if (!string.IsNullOrWhiteSpace(expression)) + if (root == AvaloniaProperty.UnsetValue) { - _node = ExpressionNodeBuilder.Build(expression, enableValidation); + root = null; } Expression = expression; + Description = description ?? expression; + _node = Parse(expression, enableDataValidation); + _root = new WeakReference(root); } /// @@ -73,24 +90,24 @@ namespace Avalonia.Markup.Data /// /// An observable which provides the root object. /// The expression. - /// Whether property validation should be enabled. + /// Whether data validation should be enabled. + /// + /// A description of the expression. If null, will be used. + /// public ExpressionObserver( IObservable rootObservable, string expression, - bool enableValidation = false) + bool enableDataValidation = false, + string description = null) { Contract.Requires(rootObservable != null); Contract.Requires(expression != null); - _rootObservable = rootObservable; - _enableValidation = enableValidation; - - if (!string.IsNullOrWhiteSpace(expression)) - { - _node = ExpressionNodeBuilder.Build(expression, enableValidation); - } - Expression = expression; + Description = description ?? expression; + _node = Parse(expression, enableDataValidation); + _finished = new Subject(); + _root = rootObservable; } /// @@ -99,27 +116,28 @@ namespace Avalonia.Markup.Data /// A function which gets the root object. /// The expression. /// An observable which triggers a re-read of the getter. - /// Whether property validation should be enabled. + /// Whether data validation should be enabled. + /// + /// A description of the expression. If null, will be used. + /// public ExpressionObserver( Func rootGetter, string expression, IObservable update, - bool enableValidation = false) + bool enableDataValidation = false, + string description = null) { Contract.Requires(rootGetter != null); Contract.Requires(expression != null); Contract.Requires(update != null); - _rootGetter = rootGetter; - _update = update; - _enableValidation = enableValidation; - - if (!string.IsNullOrWhiteSpace(expression)) - { - _node = ExpressionNodeBuilder.Build(expression, enableValidation); - } - Expression = expression; + Description = description ?? expression; + _node = Parse(expression, enableDataValidation); + _finished = new Subject(); + + _node.Target = new WeakReference(rootGetter()); + _root = update.Select(x => rootGetter()); } /// @@ -129,27 +147,20 @@ namespace Avalonia.Markup.Data /// The binding priority to use. /// /// True if the value could be set; false if the expression does not evaluate to a - /// property. + /// property. Note that the must be subscribed to + /// before setting the target value can work, as setting the value requires the + /// expression to be evaluated. /// public bool SetValue(object value, BindingPriority priority = BindingPriority.LocalValue) { - IncrementCount(); - - if (_rootGetter != null && _node != null) - { - _node.Target = new WeakReference(_rootGetter()); - } - - try - { - return _node?.SetValue(value, priority) ?? false; - } - finally - { - DecrementCount(); - } + return (Leaf as PropertyAccessorNode)?.SetTargetValue(value, priority) ?? false; } + /// + /// Gets a description of the expression being observed. + /// + public string Description { get; } + /// /// Gets the expression being observed. /// @@ -159,41 +170,7 @@ namespace Avalonia.Markup.Data /// Gets the type of the expression result or null if the expression could not be /// evaluated. /// - public Type ResultType - { - get - { - IncrementCount(); - - try - { - if (_node != null) - { - return (Leaf as PropertyAccessorNode)?.PropertyType; - } - else if (_rootGetter != null) - { - return _rootGetter()?.GetType(); - } - else - { - return _root.Target?.GetType(); - } - } - finally - { - DecrementCount(); - } - } - } - - /// - string IDescription.Description => Expression; - - /// - /// Gets the root expression node. Used for testing. - /// - internal ExpressionNode Node => _node; + public Type ResultType => (Leaf as PropertyAccessorNode)?.PropertyType; /// /// Gets the leaf node. @@ -211,94 +188,88 @@ namespace Avalonia.Markup.Data /// protected override IDisposable SubscribeCore(IObserver observer) { - IncrementCount(); - - if (_node != null) + if (_result == null) { - IObservable source = _node; + var source = (IObservable)_node; - if (_rootObservable != null) - { - source = source.TakeUntil(_rootObservable.LastOrDefaultAsync()); - } - else if (_update != null) + if (_finished != null) { - source = source.TakeUntil(_update.LastOrDefaultAsync()); + source = source.TakeUntil(_finished); } - var subscription = source.Subscribe(observer); - - return Disposable.Create(() => - { - DecrementCount(); - subscription.Dispose(); - }); + _result = Observable.Using(StartRoot, _ => source) + .Select(ToWeakReference) + .Publish(UninitializedValue) + .RefCount() + .Where(x => x != UninitializedValue) + .Select(Translate); } - else if (_rootObservable != null) + + return _result.Subscribe(observer); + } + + private static ExpressionNode Parse(string expression, bool enableDataValidation) + { + if (!string.IsNullOrWhiteSpace(expression)) { - return _rootObservable.Subscribe(observer); + return ExpressionNodeBuilder.Build(expression, enableDataValidation); } else { - if (_update == null) - { - return Observable.Never() - .StartWith(_root.Target) - .Subscribe(observer); - } - else - { - return _update - .Select(_ => _rootGetter()) - .StartWith(_rootGetter()) - .Subscribe(observer); - } + return new EmptyExpressionNode(); } } - private void IncrementCount() + private static object ToWeakReference(object o) + { + return o is BindingNotification ? o : new WeakReference(o); + } + + private object Translate(object o) { - if (_count++ == 0 && _node != null) + var weak = o as WeakReference; + + if (weak != null) { - if (_rootGetter != null) - { - _node.Target = new WeakReference(_rootGetter()); + return weak.Target; + } + else + { + var broken = BindingNotification.ExtractError(o) as MarkupBindingChainNullException; - if (_update != null) + if (broken != null) + { + // We've received notification of a broken expression due to a null value + // somewhere in the chain. If this null value occurs at the first node then we + // ignore it, as its likely that e.g. the DataContext has not yet been set up. + if (broken.HasNodes) { - _updateSubscription = _update.Subscribe(x => - _node.Target = new WeakReference(_rootGetter())); + broken.Commit(Description); + } + else + { + o = AvaloniaProperty.UnsetValue; } } - else if (_rootObservable != null) - { - _rootObserverSubscription = _rootObservable.Subscribe(x => - _node.Target = new WeakReference(x)); - } - else - { - _node.Target = _root; - } + return o; } } - private void DecrementCount() + private IDisposable StartRoot() { - if (--_count == 0 && _node != null) - { - if (_rootObserverSubscription != null) - { - _rootObserverSubscription.Dispose(); - _rootObserverSubscription = null; - } + var observable = _root as IObservable; - if (_updateSubscription != null) - { - _updateSubscription.Dispose(); - _updateSubscription = null; - } - - _node.Target = null; + if (observable != null) + { + return observable.Subscribe( + x => _node.Target = new WeakReference(x != AvaloniaProperty.UnsetValue ? x : null), + _ => _finished.OnNext(Unit.Default), + () => _finished.OnNext(Unit.Default)); + } + else + { + _node.Target = (WeakReference)_root; + return Disposable.Empty; } } } diff --git a/src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs b/src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs deleted file mode 100644 index 0a3be26c18..0000000000 --- a/src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs +++ /dev/null @@ -1,213 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Globalization; -using System.Reactive.Linq; -using System.Reactive.Subjects; -using Avalonia.Data; -using Avalonia.Logging; -using Avalonia.Utilities; - -namespace Avalonia.Markup.Data -{ - /// - /// Turns an into a subject that can be bound two-way with - /// a value converter. - /// - public class ExpressionSubject : ISubject, IDescription - { - private readonly ExpressionObserver _inner; - private readonly Type _targetType; - private readonly object _fallbackValue; - private readonly BindingPriority _priority; - - /// - /// Initializes a new instance of the class. - /// - /// The . - /// The type to convert the value to. - public ExpressionSubject(ExpressionObserver inner, Type targetType) - : this(inner, targetType, DefaultValueConverter.Instance) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The . - /// The type to convert the value to. - /// The value converter to use. - /// - /// A parameter to pass to . - /// - /// The binding priority. - public ExpressionSubject( - ExpressionObserver inner, - Type targetType, - IValueConverter converter, - object converterParameter = null, - BindingPriority priority = BindingPriority.LocalValue) - : this(inner, targetType, AvaloniaProperty.UnsetValue, converter, converterParameter, priority) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The . - /// The type to convert the value to. - /// - /// The value to use when the binding is unable to produce a value. - /// - /// The value converter to use. - /// - /// A parameter to pass to . - /// - /// The binding priority. - public ExpressionSubject( - ExpressionObserver inner, - Type targetType, - object fallbackValue, - IValueConverter converter, - object converterParameter = null, - BindingPriority priority = BindingPriority.LocalValue) - { - Contract.Requires(inner != null); - Contract.Requires(targetType != null); - Contract.Requires(converter != null); - - _inner = inner; - _targetType = targetType; - Converter = converter; - ConverterParameter = converterParameter; - _fallbackValue = fallbackValue; - _priority = priority; - } - - /// - /// Gets the converter to use on the expression. - /// - public IValueConverter Converter { get; } - - /// - /// Gets a parameter to pass to . - /// - public object ConverterParameter { get; } - - /// - string IDescription.Description => _inner.Expression; - - /// - public void OnCompleted() - { - } - - /// - public void OnError(Exception error) - { - } - - /// - public void OnNext(object value) - { - var type = _inner.ResultType; - - if (type != null) - { - var converted = Converter.ConvertBack( - value, - type, - ConverterParameter, - CultureInfo.CurrentUICulture); - - if (converted == AvaloniaProperty.UnsetValue) - { - converted = TypeUtilities.Default(type); - _inner.SetValue(converted, _priority); - } - else if (converted is BindingError) - { - var error = converted as BindingError; - - Logger.Error( - LogArea.Binding, - this, - "Error binding to {Expression}: {Message}", - _inner.Expression, - error.Exception.Message); - - if (_fallbackValue != AvaloniaProperty.UnsetValue) - { - if (TypeUtilities.TryConvert( - type, - _fallbackValue, - CultureInfo.InvariantCulture, - out converted)) - { - _inner.SetValue(converted, _priority); - } - else - { - Logger.Error( - LogArea.Binding, - this, - "Could not convert FallbackValue {FallbackValue} to {Type}", - _fallbackValue, - type); - } - } - } - else - { - _inner.SetValue(converted, _priority); - } - } - } - - /// - public IDisposable Subscribe(IObserver observer) - { - return _inner.Select(ConvertValue).Subscribe(observer); - } - - private object ConvertValue(object value) - { - var converted = - value as BindingError ?? - value as IValidationStatus ?? - Converter.Convert( - value, - _targetType, - ConverterParameter, - CultureInfo.CurrentUICulture); - - if (_fallbackValue != AvaloniaProperty.UnsetValue && - (converted == AvaloniaProperty.UnsetValue || - converted is BindingError)) - { - var error = converted as BindingError; - - if (TypeUtilities.TryConvert( - _targetType, - _fallbackValue, - CultureInfo.InvariantCulture, - out converted)) - { - if (error != null) - { - converted = new BindingError(error.Exception, converted); - } - } - else - { - converted = new BindingError( - new InvalidCastException( - $"Could not convert FallbackValue '{_fallbackValue}' to '{_targetType}'")); - } - } - - return converted; - } - } -} diff --git a/src/Markup/Avalonia.Markup/Data/IndexerNode.cs b/src/Markup/Avalonia.Markup/Data/IndexerNode.cs index 8849e7edbc..0a6b93bad1 100644 --- a/src/Markup/Avalonia.Markup/Data/IndexerNode.cs +++ b/src/Markup/Avalonia.Markup/Data/IndexerNode.cs @@ -10,130 +10,49 @@ using System.ComponentModel; using System.Globalization; using System.Linq; using System.Reflection; +using System.Reactive.Linq; namespace Avalonia.Markup.Data { - internal class IndexerNode : ExpressionNode, - IWeakSubscriber, - IWeakSubscriber + internal class IndexerNode : ExpressionNode { public IndexerNode(IList arguments) { Arguments = arguments; } - public IList Arguments { get; } + public override string Description => "[" + string.Join(",", Arguments) + "]"; - void IWeakSubscriber.OnEvent(object sender, NotifyCollectionChangedEventArgs e) + protected override IObservable StartListeningCore(WeakReference reference) { - var update = false; - if (sender is IList) - { - object indexObject; - if (!TypeUtilities.TryConvert(typeof(int), Arguments[0], CultureInfo.InvariantCulture, out indexObject)) - { - return; - } - var index = (int)indexObject; - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - update = index >= e.NewStartingIndex; - break; - case NotifyCollectionChangedAction.Remove: - update = index >= e.OldStartingIndex; - break; - case NotifyCollectionChangedAction.Replace: - update = index >= e.NewStartingIndex && - index < e.NewStartingIndex + e.NewItems.Count; - break; - case NotifyCollectionChangedAction.Move: - update = (index >= e.NewStartingIndex && - index < e.NewStartingIndex + e.NewItems.Count) || - (index >= e.OldStartingIndex && - index < e.OldStartingIndex + e.OldItems.Count); - break; - case NotifyCollectionChangedAction.Reset: - update = true; - break; - } - } - else - { - update = true; - } - - if (update) - { - CurrentValue = new WeakReference(GetValue(sender)); - } - } - - void IWeakSubscriber.OnEvent(object sender, PropertyChangedEventArgs e) - { - var typeInfo = sender.GetType().GetTypeInfo(); - - if (typeInfo.GetDeclaredProperty(e.PropertyName) == null) - { - return; - } - - if (typeInfo.GetDeclaredProperty(e.PropertyName).GetIndexParameters().Any()) - { - CurrentValue = new WeakReference(GetValue(sender)); - } - } - - protected override void SubscribeAndUpdate(WeakReference reference) - { - object target = reference.Target; - - CurrentValue = new WeakReference(GetValue(target)); - + var target = reference.Target; var incc = target as INotifyCollectionChanged; - - if (incc != null) - { - WeakSubscriptionManager.Subscribe( - incc, - nameof(incc.CollectionChanged), - this); - } - var inpc = target as INotifyPropertyChanged; - - if (inpc != null) - { - WeakSubscriptionManager.Subscribe( - inpc, - nameof(inpc.PropertyChanged), - this); - } - } - - protected override void Unsubscribe(object target) - { - var incc = target as INotifyCollectionChanged; + var inputs = new List>(); if (incc != null) { - WeakSubscriptionManager.Unsubscribe( - incc, - nameof(incc.CollectionChanged), - this); + inputs.Add(WeakObservable.FromEventPattern( + target, + nameof(incc.CollectionChanged)) + .Where(x => ShouldUpdate(x.Sender, x.EventArgs)) + .Select(_ => GetValue(target))); } - var inpc = target as INotifyPropertyChanged; - if (inpc != null) { - WeakSubscriptionManager.Unsubscribe( - inpc, - nameof(inpc.PropertyChanged), - this); + inputs.Add(WeakObservable.FromEventPattern( + target, + nameof(inpc.PropertyChanged)) + .Where(x => ShouldUpdate(x.Sender, x.EventArgs)) + .Select(_ => GetValue(target))); } + + return Observable.Merge(inputs).StartWith(GetValue(target)); } + public IList Arguments { get; } + private object GetValue(object target) { var typeInfo = target.GetType().GetTypeInfo(); @@ -141,18 +60,23 @@ namespace Avalonia.Markup.Data var dictionary = target as IDictionary; var indexerProperty = GetIndexer(typeInfo); var indexerParameters = indexerProperty?.GetIndexParameters(); + if (indexerProperty != null && indexerParameters.Length == Arguments.Count) { var convertedObjectArray = new object[indexerParameters.Length]; + for (int i = 0; i < Arguments.Count; i++) { object temp = null; + if (!TypeUtilities.TryConvert(indexerParameters[i].ParameterType, Arguments[i], CultureInfo.InvariantCulture, out temp)) { return AvaloniaProperty.UnsetValue; } + convertedObjectArray[i] = temp; } + var intArgs = convertedObjectArray.OfType().ToArray(); // Try special cases where we can validate indicies @@ -166,16 +90,18 @@ namespace Avalonia.Markup.Data { if (intArgs.Length == Arguments.Count && intArgs[0] >= 0 && intArgs[0] < list.Count) { - return list[intArgs[0]]; + return list[intArgs[0]]; } + return AvaloniaProperty.UnsetValue; } else if (dictionary != null) { if (dictionary.Contains(convertedObjectArray[0])) { - return dictionary[convertedObjectArray[0]]; + return dictionary[convertedObjectArray[0]]; } + return AvaloniaProperty.UnsetValue; } else @@ -187,11 +113,11 @@ namespace Avalonia.Markup.Data else { // Fallback to unchecked access - return indexerProperty.GetValue(target, convertedObjectArray); + return indexerProperty.GetValue(target, convertedObjectArray); } } // Multidimensional arrays end up here because the indexer search picks up the IList indexer instead of the - // multidimensional indexer, which doesn't take the same number of arguments + // multidimensional indexer, which doesn't take the same number of arguments else if (typeInfo.IsArray) { return GetValueFromArray((Array)target); @@ -220,13 +146,16 @@ namespace Avalonia.Markup.Data private bool ConvertArgumentsToInts(out int[] intArgs) { intArgs = new int[Arguments.Count]; + for (int i = 0; i < Arguments.Count; ++i) { object value; + if (!TypeUtilities.TryConvert(typeof(int), Arguments[i], CultureInfo.InvariantCulture, out value)) { return false; } + intArgs[i] = (int)value; } return true; @@ -235,7 +164,8 @@ namespace Avalonia.Markup.Data private static PropertyInfo GetIndexer(TypeInfo typeInfo) { PropertyInfo indexer; - for (;typeInfo != null; typeInfo = typeInfo.BaseType?.GetTypeInfo()) + + for (; typeInfo != null; typeInfo = typeInfo.BaseType?.GetTypeInfo()) { // Check for the default indexer name first to make this faster. // This will only be false when a class in VB has a custom indexer name. @@ -243,14 +173,16 @@ namespace Avalonia.Markup.Data { return indexer; } + foreach (var property in typeInfo.DeclaredProperties) { if (property.GetIndexParameters().Any()) { return property; } - } + } } + return null; } @@ -273,5 +205,46 @@ namespace Avalonia.Markup.Data return false; } } + + private bool ShouldUpdate(object sender, NotifyCollectionChangedEventArgs e) + { + if (sender is IList) + { + object indexObject; + + if (!TypeUtilities.TryConvert(typeof(int), Arguments[0], CultureInfo.InvariantCulture, out indexObject)) + { + return false; + } + + var index = (int)indexObject; + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + return index >= e.NewStartingIndex; + case NotifyCollectionChangedAction.Remove: + return index >= e.OldStartingIndex; + case NotifyCollectionChangedAction.Replace: + return index >= e.NewStartingIndex && + index < e.NewStartingIndex + e.NewItems.Count; + case NotifyCollectionChangedAction.Move: + return (index >= e.NewStartingIndex && + index < e.NewStartingIndex + e.NewItems.Count) || + (index >= e.OldStartingIndex && + index < e.OldStartingIndex + e.OldItems.Count); + case NotifyCollectionChangedAction.Reset: + return true; + } + } + + return false; + } + + private bool ShouldUpdate(object sender, PropertyChangedEventArgs e) + { + var typeInfo = sender.GetType().GetTypeInfo(); + return typeInfo.GetDeclaredProperty(e.PropertyName)?.GetIndexParameters().Any() ?? false; + } } } diff --git a/src/Markup/Avalonia.Markup/Data/LogicalNotNode.cs b/src/Markup/Avalonia.Markup/Data/LogicalNotNode.cs index 29fa842439..58a7915254 100644 --- a/src/Markup/Avalonia.Markup/Data/LogicalNotNode.cs +++ b/src/Markup/Avalonia.Markup/Data/LogicalNotNode.cs @@ -3,21 +3,17 @@ using System; using System.Globalization; -using System.Reactive.Linq; using Avalonia.Data; namespace Avalonia.Markup.Data { internal class LogicalNotNode : ExpressionNode { - public override bool SetValue(object value, BindingPriority priority) - { - return false; - } + public override string Description => "!"; - public override IDisposable Subscribe(IObserver observer) + protected override void NextValueChanged(object value) { - return Next.Select(Negate).Subscribe(observer); + base.NextValueChanged(Negate(value)); } private static object Negate(object v) @@ -34,6 +30,12 @@ namespace Avalonia.Markup.Data { return !result; } + else + { + return new BindingNotification( + new InvalidCastException($"Unable to convert '{s}' to bool."), + BindingErrorType.Error); + } } else { @@ -42,9 +44,17 @@ namespace Avalonia.Markup.Data var boolean = Convert.ToBoolean(v, CultureInfo.InvariantCulture); return !boolean; } - catch + catch (InvalidCastException) + { + // The error message here is "Unable to cast object of type 'System.Object' + // to type 'System.IConvertible'" which is kinda useless so provide our own. + return new BindingNotification( + new InvalidCastException($"Unable to convert '{v}' to bool."), + BindingErrorType.Error); + } + catch (Exception e) { - // TODO: Maybe should log something here. + return new BindingNotification(e, BindingErrorType.Error); } } } diff --git a/src/Markup/Avalonia.Markup/Data/MarkupBindingChainNullException.cs b/src/Markup/Avalonia.Markup/Data/MarkupBindingChainNullException.cs new file mode 100644 index 0000000000..a549d6ebb6 --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/MarkupBindingChainNullException.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Linq; +using Avalonia.Data; + +namespace Avalonia.Markup.Data +{ + internal class MarkupBindingChainNullException : BindingChainNullException + { + private IList _nodes = new List(); + + public MarkupBindingChainNullException() + { + } + + public MarkupBindingChainNullException(string expression, string expressionNullPoint) + : base(expression, expressionNullPoint) + { + _nodes = null; + } + + public bool HasNodes => _nodes.Count > 0; + public void AddNode(string node) => _nodes.Add(node); + + public void Commit(string expression) + { + Expression = expression; + ExpressionNullPoint = string.Join(".", _nodes.Reverse()) + .Replace(".!", "!") + .Replace(".[", "["); + _nodes = null; + } + } +} diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/AvaloniaPropertyAccessorPlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/AvaloniaPropertyAccessorPlugin.cs index b2d73ab8fc..1d4a9a688e 100644 --- a/src/Markup/Avalonia.Markup/Data/Plugins/AvaloniaPropertyAccessorPlugin.cs +++ b/src/Markup/Avalonia.Markup/Data/Plugins/AvaloniaPropertyAccessorPlugin.cs @@ -4,7 +4,6 @@ using System; using System.Reactive.Linq; using Avalonia.Data; -using Avalonia.Logging; namespace Avalonia.Markup.Data.Plugins { @@ -13,36 +12,22 @@ namespace Avalonia.Markup.Data.Plugins /// public class AvaloniaPropertyAccessorPlugin : IPropertyAccessorPlugin { - /// - /// Checks whether this plugin can handle accessing the properties of the specified object. - /// - /// A weak reference to the object. - /// True if the plugin can handle the object; otherwise false. - public bool Match(WeakReference reference) - { - Contract.Requires(reference != null); - - return reference.Target is AvaloniaObject; - } + /// + public bool Match(WeakReference reference) => reference.Target is AvaloniaObject; /// /// Starts monitoring the value of a property on an object. /// /// A weak reference to the object. /// The property name. - /// A function to call when the property changes. /// /// An interface through which future interactions with the /// property will be made. /// - public IPropertyAccessor Start( - WeakReference reference, - string propertyName, - Action changed) + public IPropertyAccessor Start(WeakReference reference, string propertyName) { Contract.Requires(reference != null); Contract.Requires(propertyName != null); - Contract.Requires(changed != null); var instance = reference.Target; var o = (AvaloniaObject)instance; @@ -50,13 +35,13 @@ namespace Avalonia.Markup.Data.Plugins if (p != null) { - return new Accessor(new WeakReference(o), p, changed); + return new Accessor(new WeakReference(o), p); } else if (instance != AvaloniaProperty.UnsetValue) { var message = $"Could not find AvaloniaProperty '{propertyName}' on '{instance}'"; var exception = new MissingMemberException(message); - return new PropertyError(new BindingError(exception)); + return new PropertyError(new BindingNotification(exception, BindingErrorType.Error)); } else { @@ -64,23 +49,19 @@ namespace Avalonia.Markup.Data.Plugins } } - private class Accessor : IPropertyAccessor + private class Accessor : PropertyAccessorBase { private readonly WeakReference _reference; private readonly AvaloniaProperty _property; private IDisposable _subscription; - public Accessor( - WeakReference reference, - AvaloniaProperty property, - Action changed) + public Accessor(WeakReference reference, AvaloniaProperty property) { Contract.Requires(reference != null); Contract.Requires(property != null); _reference = reference; _property = property; - _subscription = Instance.GetWeakObservable(property).Skip(1).Subscribe(changed); } public AvaloniaObject Instance @@ -93,17 +74,10 @@ namespace Avalonia.Markup.Data.Plugins } } - public Type PropertyType => _property.PropertyType; - - public object Value => Instance.GetValue(_property); + public override Type PropertyType => _property.PropertyType; + public override object Value => Instance?.GetValue(_property); - public void Dispose() - { - _subscription?.Dispose(); - _subscription = null; - } - - public bool SetValue(object value, BindingPriority priority) + public override bool SetValue(object value, BindingPriority priority) { if (!_property.IsReadOnly) { @@ -113,6 +87,17 @@ namespace Avalonia.Markup.Data.Plugins return false; } + + protected override void Dispose(bool disposing) + { + _subscription?.Dispose(); + _subscription = null; + } + + protected override void SubscribeCore(IObserver observer) + { + _subscription = Instance.GetWeakObservable(_property).Subscribe(observer); + } } } } diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/DataAnnotationsValidationPlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/DataAnnotationsValidationPlugin.cs new file mode 100644 index 0000000000..859438636a --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/Plugins/DataAnnotationsValidationPlugin.cs @@ -0,0 +1,81 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Reflection; +using Avalonia.Data; + +namespace Avalonia.Markup.Data.Plugins +{ + /// + /// Validates properties on that have s. + /// + public class DataAnnotationsValidationPlugin : IDataValidationPlugin + { + /// + public bool Match(WeakReference reference, string memberName) + { + return reference.Target? + .GetType() + .GetRuntimeProperty(memberName)? + .GetCustomAttributes() + .Any() ?? false; + } + + /// + public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor inner) + { + return new Accessor(reference, name, inner); + } + + private class Accessor : DataValidatiorBase + { + private ValidationContext _context; + + public Accessor(WeakReference reference, string name, IPropertyAccessor inner) + : base(inner) + { + _context = new ValidationContext(reference.Target); + _context.MemberName = name; + } + + public override bool SetValue(object value, BindingPriority priority) + { + return base.SetValue(value, priority); + } + + protected override void InnerValueChanged(object value) + { + var errors = new List(); + + if (Validator.TryValidateProperty(value, _context, errors)) + { + base.InnerValueChanged(value); + } + else + { + base.InnerValueChanged(new BindingNotification( + CreateException(errors), + BindingErrorType.DataValidationError, + value)); + } + } + + private Exception CreateException(IList errors) + { + if (errors.Count == 1) + { + return new ValidationException(errors[0].ErrorMessage); + } + else + { + return new AggregateException( + errors.Select(x => new ValidationException(x.ErrorMessage))); + } + } + } + } +} diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/DataValidatiorBase.cs b/src/Markup/Avalonia.Markup/Data/Plugins/DataValidatiorBase.cs new file mode 100644 index 0000000000..95d269f437 --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/Plugins/DataValidatiorBase.cs @@ -0,0 +1,80 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Data; + +namespace Avalonia.Markup.Data.Plugins +{ + /// + /// Base class for data validators. + /// + /// + /// Data validators are s that are returned from an + /// . They wrap an inner + /// and convert any values received from the inner property accessor into + /// s. + /// + public abstract class DataValidatiorBase : PropertyAccessorBase, IObserver + { + private readonly IPropertyAccessor _inner; + + /// + /// Initializes a new instance of the class. + /// + /// The inner property accessor. + protected DataValidatiorBase(IPropertyAccessor inner) + { + _inner = inner; + } + + /// + public override Type PropertyType => _inner.PropertyType; + + /// + public override object Value => _inner.Value; + + /// + public override bool SetValue(object value, BindingPriority priority) => _inner.SetValue(value, priority); + + /// + /// Should never be called: the inner should never notify + /// completion. + /// + void IObserver.OnCompleted() { } + + /// + /// Should never be called: the inner should never notify + /// an error. + /// + void IObserver.OnError(Exception error) { } + + /// + /// Called when the inner notifies with a new value. + /// + /// The value. + void IObserver.OnNext(object value) => InnerValueChanged(value); + + /// + protected override void Dispose(bool disposing) => _inner.Dispose(); + + /// + /// Begins listening to the inner . + /// + protected override void SubscribeCore(IObserver observer) => _inner.Subscribe(this); + + /// + /// Called when the inner notifies with a new value. + /// + /// The value. + /// + /// Notifies the observer that the value has changed. The value will be wrapped in a + /// if it is not already a binding notification. + /// + protected virtual void InnerValueChanged(object value) + { + var notification = value as BindingNotification ?? new BindingNotification(value); + Observer.OnNext(notification); + } + } +} \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/ExceptionValidationPlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/ExceptionValidationPlugin.cs index 95a32a6928..e0b6bcfd7c 100644 --- a/src/Markup/Avalonia.Markup/Data/Plugins/ExceptionValidationPlugin.cs +++ b/src/Markup/Avalonia.Markup/Data/Plugins/ExceptionValidationPlugin.cs @@ -10,23 +10,21 @@ namespace Avalonia.Markup.Data.Plugins /// /// Validates properties that report errors by throwing exceptions. /// - public class ExceptionValidationPlugin : IValidationPlugin + public class ExceptionValidationPlugin : IDataValidationPlugin { - public static ExceptionValidationPlugin Instance { get; } = new ExceptionValidationPlugin(); - /// - public bool Match(WeakReference reference) => true; + public bool Match(WeakReference reference, string memberName) => true; /// - public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor, Action callback) + public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor inner) { - return new ExceptionValidationChecker(reference, name, accessor, callback); + return new Validator(reference, name, inner); } - private class ExceptionValidationChecker : ValidatingPropertyAccessorBase + private class Validator : DataValidatiorBase { - public ExceptionValidationChecker(WeakReference reference, string name, IPropertyAccessor accessor, Action callback) - : base(reference, name, accessor, callback) + public Validator(WeakReference reference, string name, IPropertyAccessor inner) + : base(inner) { } @@ -34,39 +32,19 @@ namespace Avalonia.Markup.Data.Plugins { try { - var success = base.SetValue(value, priority); - SendValidationCallback(new ExceptionValidationStatus(null)); - return success; + return base.SetValue(value, priority); } catch (TargetInvocationException ex) { - SendValidationCallback(new ExceptionValidationStatus(ex.InnerException)); + Observer.OnNext(new BindingNotification(ex.InnerException, BindingErrorType.DataValidationError)); } catch (Exception ex) { - SendValidationCallback(new ExceptionValidationStatus(ex)); + Observer.OnNext(new BindingNotification(ex, BindingErrorType.DataValidationError)); } - return false; - } - } - /// - /// Describes the current validation status after setting a property value. - /// - public class ExceptionValidationStatus : IValidationStatus - { - internal ExceptionValidationStatus(Exception exception) - { - Exception = exception; + return false; } - - /// - /// The thrown exception. If there was no thrown exception, null. - /// - public Exception Exception { get; } - - /// - public bool IsValid => Exception == null; } } } diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/IDataValidationPlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/IDataValidationPlugin.cs new file mode 100644 index 0000000000..0952e2edab --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/Plugins/IDataValidationPlugin.cs @@ -0,0 +1,37 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Data; + +namespace Avalonia.Markup.Data.Plugins +{ + /// + /// Defines how data validation is observed by an . + /// + public interface IDataValidationPlugin + { + /// + /// Checks whether this plugin can handle data validation on the specified object. + /// + /// A weak reference to the object. + /// The name of the member to validate. + /// True if the plugin can handle the object; otherwise false. + bool Match(WeakReference reference, string memberName); + + /// + /// Starts monitoring the data validation state of a property on an object. + /// + /// A weak reference to the object. + /// The property name. + /// The inner property accessor used to aceess the property. + /// + /// An interface through which future interactions with the + /// property will be made. + /// + IPropertyAccessor Start( + WeakReference reference, + string propertyName, + IPropertyAccessor inner); + } +} diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessor.cs b/src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessor.cs index d9da4b906f..9e686baf10 100644 --- a/src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessor.cs +++ b/src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessor.cs @@ -10,11 +10,14 @@ namespace Avalonia.Markup.Data.Plugins /// Defines an accessor to a property on an object returned by a /// /// - public interface IPropertyAccessor : IDisposable + public interface IPropertyAccessor : IObservable, IDisposable { /// /// Gets the type of the property. /// + /// + /// The accessor has not been subscribed to yet. + /// Type PropertyType { get; } /// diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessorPlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessorPlugin.cs index 0d6b57e424..187142bd0e 100644 --- a/src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessorPlugin.cs +++ b/src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessorPlugin.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections; namespace Avalonia.Markup.Data.Plugins { @@ -24,14 +23,12 @@ namespace Avalonia.Markup.Data.Plugins /// /// A weak reference to the object. /// The property name. - /// A function to call when the property changes. /// /// An interface through which future interactions with the /// property will be made. /// IPropertyAccessor Start( WeakReference reference, - string propertyName, - Action changed); + string propertyName); } } diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/IValidationPlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/IValidationPlugin.cs deleted file mode 100644 index e42e1ad7b2..0000000000 --- a/src/Markup/Avalonia.Markup/Data/Plugins/IValidationPlugin.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using Avalonia.Data; - -namespace Avalonia.Markup.Data.Plugins -{ - /// - /// Defines how view model data validation is observed by an . - /// - public interface IValidationPlugin - { - - /// - /// Checks whether the data uses a validation scheme supported by this plugin. - /// - /// A weak reference to the data. - /// true if this plugin can observe the validation; otherwise, false. - bool Match(WeakReference reference); - - /// - /// Starts monitoring the validation state of an object for the given property. - /// - /// A weak reference to the object. - /// The property name. - /// An underlying to access the property. - /// A function to call when the validation state changes. - /// - /// A subclass through which future interactions with the - /// property will be made. - /// - IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor, Action callback); - } -} diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/IValuePlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/IValuePlugin.cs new file mode 100644 index 0000000000..fb285c6d73 --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/Plugins/IValuePlugin.cs @@ -0,0 +1,29 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; + +namespace Avalonia.Markup.Data.Plugins +{ + /// + /// Defines how values are observed by an . + /// + public interface IValuePlugin + { + /// + /// Checks whether this plugin handles the specified value. + /// + /// A weak reference to the value. + /// True if the plugin can handle the value; otherwise false. + bool Match(WeakReference reference); + + /// + /// Starts producing output based on the specified value. + /// + /// A weak reference to the object. + /// + /// An observable that produces the output for the value. + /// + IObservable Start(WeakReference reference); + } +} diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/IndeiValidationPlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/IndeiValidationPlugin.cs index f6b4aea614..82bc87c207 100644 --- a/src/Markup/Avalonia.Markup/Data/Plugins/IndeiValidationPlugin.cs +++ b/src/Markup/Avalonia.Markup/Data/Plugins/IndeiValidationPlugin.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections; +using System.Collections.Generic; using System.ComponentModel; using System.Linq; using Avalonia.Data; @@ -13,79 +13,106 @@ namespace Avalonia.Markup.Data.Plugins /// /// Validates properties on objects that implement . /// - public class IndeiValidationPlugin : IValidationPlugin + public class IndeiValidationPlugin : IDataValidationPlugin { /// - public bool Match(WeakReference reference) - { - return reference.Target is INotifyDataErrorInfo; - } + public bool Match(WeakReference reference, string memberName) => reference.Target is INotifyDataErrorInfo; /// - public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor, Action callback) + public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor) { - return new IndeiValidationChecker(reference, name, accessor, callback); + return new Validator(reference, name, accessor); } - private class IndeiValidationChecker : ValidatingPropertyAccessorBase, IWeakSubscriber + private class Validator : DataValidatiorBase, IWeakSubscriber { - public IndeiValidationChecker(WeakReference reference, string name, IPropertyAccessor accessor, Action callback) - : base(reference, name, accessor, callback) + WeakReference _reference; + string _name; + + public Validator(WeakReference reference, string name, IPropertyAccessor inner) + : base(inner) + { + _reference = reference; + _name = name; + } + + void IWeakSubscriber.OnEvent(object sender, DataErrorsChangedEventArgs e) { - var target = reference.Target as INotifyDataErrorInfo; + if (e.PropertyName == _name || string.IsNullOrEmpty(e.PropertyName)) + { + Observer.OnNext(CreateBindingNotification(Value)); + } + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + var target = _reference.Target as INotifyDataErrorInfo; + if (target != null) { - if (target.HasErrors) - { - SendValidationCallback(new IndeiValidationStatus(target.GetErrors(name))); - } - WeakSubscriptionManager.Subscribe( + WeakSubscriptionManager.Unsubscribe( target, nameof(target.ErrorsChanged), this); } } - public override void Dispose() + protected override void SubscribeCore(IObserver observer) { - base.Dispose(); var target = _reference.Target as INotifyDataErrorInfo; + if (target != null) { - WeakSubscriptionManager.Unsubscribe( + WeakSubscriptionManager.Subscribe( target, nameof(target.ErrorsChanged), this); } + + base.SubscribeCore(observer); } - public void OnEvent(object sender, DataErrorsChangedEventArgs e) + protected override void InnerValueChanged(object value) { - if (e.PropertyName == _name || string.IsNullOrEmpty(e.PropertyName)) + base.InnerValueChanged(CreateBindingNotification(value)); + } + + private BindingNotification CreateBindingNotification(object value) + { + var target = (INotifyDataErrorInfo)_reference.Target; + + if (target != null) { - var indei = _reference.Target as INotifyDataErrorInfo; - SendValidationCallback(new IndeiValidationStatus(indei.GetErrors(e.PropertyName))); + var errors = target.GetErrors(_name)? + .Cast() + .Where(x => x != null).ToList(); + + if (errors?.Count > 0) + { + return new BindingNotification( + GenerateException(errors), + BindingErrorType.DataValidationError, + value); + } } + + return new BindingNotification(value); } - } - /// - /// Describes the current validation status of a property as reported by an object that implements . - /// - public class IndeiValidationStatus : IValidationStatus - { - internal IndeiValidationStatus(IEnumerable errors) + private Exception GenerateException(IList errors) { - Errors = errors; + if (errors.Count == 1) + { + return new Exception(errors[0]); + } + else + { + return new AggregateException( + errors.Select(x => new Exception(x))); + } } - - /// - public bool IsValid => !Errors?.OfType().Any() ?? true; - - /// - /// The errors on the given property and on the object as a whole. - /// - public IEnumerable Errors { get; } } } } diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs index 36046dd742..138f09b373 100644 --- a/src/Markup/Avalonia.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Markup/Avalonia.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs @@ -9,7 +9,6 @@ using System.Reflection; using Avalonia.Data; using Avalonia.Logging; using Avalonia.Utilities; -using System.Collections; namespace Avalonia.Markup.Data.Plugins { @@ -19,124 +18,130 @@ namespace Avalonia.Markup.Data.Plugins /// public class InpcPropertyAccessorPlugin : IPropertyAccessorPlugin { - /// - /// Checks whether this plugin can handle accessing the properties of the specified object. - /// - /// The object. - /// True if the plugin can handle the object; otherwise false. - public bool Match(WeakReference reference) - { - Contract.Requires(reference != null); - - return true; - } + /// + public bool Match(WeakReference reference) => true; /// /// Starts monitoring the value of a property on an object. /// /// The object. /// The property name. - /// A function to call when the property changes. /// /// An interface through which future interactions with the /// property will be made. /// - public IPropertyAccessor Start( - WeakReference reference, - string propertyName, - Action changed) + public IPropertyAccessor Start(WeakReference reference, string propertyName) { Contract.Requires(reference != null); Contract.Requires(propertyName != null); - Contract.Requires(changed != null); var instance = reference.Target; var p = instance.GetType().GetRuntimeProperties().FirstOrDefault(_ => _.Name == propertyName); if (p != null) { - return new Accessor(reference, p, changed); + return new Accessor(reference, p); } else { var message = $"Could not find CLR property '{propertyName}' on '{instance}'"; var exception = new MissingMemberException(message); - return new PropertyError(new BindingError(exception)); + return new PropertyError(new BindingNotification(exception, BindingErrorType.Error)); } } - private class Accessor : IPropertyAccessor, IWeakSubscriber + private class Accessor : PropertyAccessorBase, IWeakSubscriber { private readonly WeakReference _reference; private readonly PropertyInfo _property; - private readonly Action _changed; + private bool _eventRaised; - public Accessor( - WeakReference reference, - PropertyInfo property, - Action changed) + public Accessor(WeakReference reference, PropertyInfo property) { Contract.Requires(reference != null); Contract.Requires(property != null); _reference = reference; _property = property; - _changed = changed; + } - var inpc = reference.Target as INotifyPropertyChanged; + public override Type PropertyType => _property.PropertyType; - if (inpc != null) + public override object Value + { + get { - WeakSubscriptionManager.Subscribe( - inpc, - nameof(inpc.PropertyChanged), - this); + var o = _reference.Target; + return (o != null) ? _property.GetValue(o) : null; } - else + } + + public override bool SetValue(object value, BindingPriority priority) + { + if (_property.CanWrite) { - Logger.Warning( - LogArea.Binding, - this, - "Bound to property {Property} on {Source} which does not implement INotifyPropertyChanged", - property.Name, - reference.Target, - reference.Target.GetType()); + _eventRaised = false; + _property.SetValue(_reference.Target, value); + + if (!_eventRaised) + { + SendCurrentValue(); + } + + return true; } - } - public Type PropertyType => _property.PropertyType; + return false; + } - public object Value => _property.GetValue(_reference.Target); + void IWeakSubscriber.OnEvent(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName)) + { + _eventRaised = true; + SendCurrentValue(); + } + } - public void Dispose() + protected override void Dispose(bool disposing) { var inpc = _reference.Target as INotifyPropertyChanged; if (inpc != null) { - WeakSubscriptionManager.Unsubscribe( + WeakSubscriptionManager.Unsubscribe( inpc, nameof(inpc.PropertyChanged), this); } } - public bool SetValue(object value, BindingPriority priority) + protected override void SubscribeCore(IObserver observer) { - if (_property.CanWrite) + SendCurrentValue(); + SubscribeToChanges(); + } + + private void SendCurrentValue() + { + try { - _property.SetValue(_reference.Target, value); - return true; + var value = Value; + Observer.OnNext(value); } - - return false; + catch { } } - void IWeakSubscriber.OnEvent(object sender, PropertyChangedEventArgs e) + private void SubscribeToChanges() { - if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName)) + var inpc = _reference.Target as INotifyPropertyChanged; + + if (inpc != null) { - _changed(Value); + WeakSubscriptionManager.Subscribe( + inpc, + nameof(inpc.PropertyChanged), + this); } } } diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/ObservableValuePlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/ObservableValuePlugin.cs new file mode 100644 index 0000000000..a406fc55b9 --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/Plugins/ObservableValuePlugin.cs @@ -0,0 +1,44 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Reflection; +using System.Threading.Tasks; +using System.Windows.Input; +using Avalonia.Data; + +namespace Avalonia.Markup.Data.Plugins +{ + /// + /// Handles binding to s in an . + /// + public class ObservableValuePlugin : IValuePlugin + { + /// + /// Checks whether this plugin handles the specified value. + /// + /// A weak reference to the value. + /// True if the plugin can handle the value; otherwise false. + public virtual bool Match(WeakReference reference) + { + var target = reference.Target; + + // ReactiveCommand is an IObservable but we want to bind to it, not its value. + return target is IObservable && !(target is ICommand); + } + + /// + /// Starts producing output based on the specified value. + /// + /// A weak reference to the object. + /// + /// An observable that produces the output for the value. + /// + public virtual IObservable Start(WeakReference reference) + { + return reference.Target as IObservable; + } + } +} diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/PropertyAccessorBase.cs b/src/Markup/Avalonia.Markup/Data/Plugins/PropertyAccessorBase.cs new file mode 100644 index 0000000000..9aa858e0eb --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/Plugins/PropertyAccessorBase.cs @@ -0,0 +1,68 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Data; + +namespace Avalonia.Markup.Data.Plugins +{ + /// + /// Defines a default base implementation for a . + /// + /// + /// is an observable that will only be subscribed to one time. + /// In addition, the subscription can be disposed by calling on the + /// property accessor itself - this prevents needing to hold two references for a subscription. + /// + public abstract class PropertyAccessorBase : IPropertyAccessor + { + /// + public abstract Type PropertyType { get; } + + /// + public abstract object Value { get; } + + /// + /// Stops the subscription. + /// + public void Dispose() => Dispose(true); + + /// + public abstract bool SetValue(object value, BindingPriority priority); + + /// + /// The currently subscribed observer. + /// + protected IObserver Observer { get; private set; } + + /// + public IDisposable Subscribe(IObserver observer) + { + Contract.Requires(observer != null); + + if (Observer != null) + { + throw new InvalidOperationException( + "A property accessor can be subscribed to only once."); + } + + Observer = observer; + SubscribeCore(observer); + return this; + } + + /// + /// Stops listening to the property. + /// + /// + /// True if the method was called, false if the object is being + /// finalized. + /// + protected virtual void Dispose(bool disposing) => Observer = null; + + /// + /// When overridden in a derived class, begins listening to the property. + /// + protected abstract void SubscribeCore(IObserver observer); + } +} diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/PropertyError.cs b/src/Markup/Avalonia.Markup/Data/Plugins/PropertyError.cs index f73bb1fc94..b351ef39bd 100644 --- a/src/Markup/Avalonia.Markup/Data/Plugins/PropertyError.cs +++ b/src/Markup/Avalonia.Markup/Data/Plugins/PropertyError.cs @@ -1,4 +1,5 @@ using System; +using System.Reactive.Disposables; using Avalonia.Data; namespace Avalonia.Markup.Data.Plugins @@ -8,13 +9,13 @@ namespace Avalonia.Markup.Data.Plugins /// public class PropertyError : IPropertyAccessor { - private BindingError _error; + private BindingNotification _error; /// /// Initializes a new instance of the class. /// /// The error to report. - public PropertyError(BindingError error) + public PropertyError(BindingNotification error) { _error = error; } @@ -35,5 +36,11 @@ namespace Avalonia.Markup.Data.Plugins { return false; } + + public IDisposable Subscribe(IObserver observer) + { + observer.OnNext(_error); + return Disposable.Empty; + } } } diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/TaskValuePlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/TaskValuePlugin.cs new file mode 100644 index 0000000000..b6fda67503 --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/Plugins/TaskValuePlugin.cs @@ -0,0 +1,82 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Reactive.Concurrency; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Reflection; +using System.Threading.Tasks; +using Avalonia.Data; + +namespace Avalonia.Markup.Data.Plugins +{ + /// + /// Handles binding to s in an . + /// + public class TaskValuePlugin : IValuePlugin + { + /// + /// Checks whether this plugin handles the specified value. + /// + /// A weak reference to the value. + /// True if the plugin can handle the value; otherwise false. + public virtual bool Match(WeakReference reference) => reference.Target is Task; + + /// + /// Starts producing output based on the specified value. + /// + /// A weak reference to the object. + /// + /// An observable that produces the output for the value. + /// + public virtual IObservable Start(WeakReference reference) + { + var task = reference.Target as Task; + + if (task != null) + { + var resultProperty = task.GetType().GetTypeInfo().GetDeclaredProperty("Result"); + + if (resultProperty != null) + { + switch (task.Status) + { + case TaskStatus.RanToCompletion: + case TaskStatus.Faulted: + return HandleCompleted(task); + default: + var subject = new Subject(); + task.ContinueWith( + x => HandleCompleted(task).Subscribe(subject), + TaskScheduler.FromCurrentSynchronizationContext()) + .ConfigureAwait(false); + return subject; + } + } + } + + return Observable.Empty(); + } + + protected IObservable HandleCompleted(Task task) + { + var resultProperty = task.GetType().GetTypeInfo().GetDeclaredProperty("Result"); + + if (resultProperty != null) + { + switch (task.Status) + { + case TaskStatus.RanToCompletion: + return Observable.Return(resultProperty.GetValue(task)); + case TaskStatus.Faulted: + return Observable.Return(new BindingNotification(task.Exception, BindingErrorType.Error)); + default: + throw new AvaloniaInternalException("HandleCompleted called for non-completed Task."); + } + } + + return Observable.Empty(); + } + } +} diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/ValidatingPropertyAccessorBase.cs b/src/Markup/Avalonia.Markup/Data/Plugins/ValidatingPropertyAccessorBase.cs deleted file mode 100644 index 07ac600a8a..0000000000 --- a/src/Markup/Avalonia.Markup/Data/Plugins/ValidatingPropertyAccessorBase.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using Avalonia.Data; - -namespace Avalonia.Markup.Data.Plugins -{ - - /// - /// A base class for validating s that wraps an and forwards method calls to it. - /// - public abstract class ValidatingPropertyAccessorBase : IPropertyAccessor - { - protected readonly WeakReference _reference; - protected readonly string _name; - private readonly IPropertyAccessor _accessor; - private readonly Action _callback; - - protected ValidatingPropertyAccessorBase(WeakReference reference, string name, IPropertyAccessor accessor, Action callback) - { - _reference = reference; - _name = name; - _accessor = accessor; - _callback = callback; - } - - /// - public Type PropertyType => _accessor.PropertyType; - - /// - public object Value => _accessor.Value; - - /// - public virtual void Dispose() => _accessor.Dispose(); - - /// - public virtual bool SetValue(object value, BindingPriority priority) => _accessor.SetValue(value, priority); - - /// - /// Sends the validation status to the callback specified in construction. - /// - /// The validation status. - protected void SendValidationCallback(IValidationStatus status) - { - _callback?.Invoke(status); - } - } -} \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup/Data/PropertyAccessorNode.cs b/src/Markup/Avalonia.Markup/Data/PropertyAccessorNode.cs index b12fbe90eb..7fb3137417 100644 --- a/src/Markup/Avalonia.Markup/Data/PropertyAccessorNode.cs +++ b/src/Markup/Avalonia.Markup/Data/PropertyAccessorNode.cs @@ -3,11 +3,8 @@ using System; using System.Linq; +using System.Reactive.Disposables; using System.Reactive.Linq; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using System.Windows.Input; using Avalonia.Data; using Avalonia.Markup.Data.Plugins; @@ -15,9 +12,8 @@ namespace Avalonia.Markup.Data { internal class PropertyAccessorNode : ExpressionNode { + private readonly bool _enableValidation; private IPropertyAccessor _accessor; - private IDisposable _subscription; - private bool _enableValidation; public PropertyAccessorNode(string propertyName, bool enableValidation) { @@ -25,118 +21,44 @@ namespace Avalonia.Markup.Data _enableValidation = enableValidation; } + public override string Description => PropertyName; public string PropertyName { get; } - public Type PropertyType => _accessor?.PropertyType; - public override bool SetValue(object value, BindingPriority priority) + public bool SetTargetValue(object value, BindingPriority priority) { - if (Next != null) + if (_accessor != null) { - return Next.SetValue(value, priority); + try { return _accessor.SetValue(value, priority); } catch { } } - else - { - if (_accessor != null) - { - return _accessor.SetValue(value, priority); - } - return false; - } + return false; } - protected override void SubscribeAndUpdate(WeakReference reference) + protected override IObservable StartListeningCore(WeakReference reference) { - var instance = reference.Target; + var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference)); + var accessor = plugin?.Start(reference, PropertyName); - if (instance != null && instance != AvaloniaProperty.UnsetValue) + if (_enableValidation && Next == null) { - var accessorPlugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference)); - - if (accessorPlugin != null) + foreach (var validator in ExpressionObserver.DataValidators) { - _accessor = ExceptionValidationPlugin.Instance.Start( - reference, - PropertyName, - accessorPlugin.Start(reference, PropertyName, SetCurrentValue), - SendValidationStatus); - - if (_enableValidation) - { - foreach (var validationPlugin in ExpressionObserver.ValidationCheckers) - { - if (validationPlugin.Match(reference)) - { - _accessor = validationPlugin.Start(reference, PropertyName, _accessor, SendValidationStatus); - } - } - } - - if (_accessor != null) + if (validator.Match(reference, PropertyName)) { - SetCurrentValue(_accessor.Value); - return; + accessor = validator.Start(reference, PropertyName, accessor); } } } - CurrentValue = UnsetReference; - } - - protected override void Unsubscribe(object target) - { - _accessor?.Dispose(); - _accessor = null; - } - - private void SetCurrentValue(object value) - { - var observable = value as IObservable; - var command = value as ICommand; - var task = value as Task; - bool set = false; - - // HACK: ReactiveCommand is an IObservable but we want to bind to it, not its value. - // We may need to make this a more general solution. - if (observable != null && command == null) - { - CurrentValue = UnsetReference; - set = true; - _subscription = observable - .ObserveOn(SynchronizationContext.Current) - .Subscribe(x => CurrentValue = new WeakReference(x)); - } - else if (task != null) - { - var resultProperty = task.GetType().GetTypeInfo().GetDeclaredProperty("Result"); - - if (resultProperty != null) + // Ensure that _accessor is set for the duration of the subscription. + return Observable.Using( + () => { - if (task.Status == TaskStatus.RanToCompletion) - { - CurrentValue = new WeakReference(resultProperty.GetValue(task)); - set = true; - } - else - { - task.ContinueWith( - x => CurrentValue = new WeakReference(resultProperty.GetValue(task)), - TaskScheduler.FromCurrentSynchronizationContext()) - .ConfigureAwait(false); - } - } - } - else - { - CurrentValue = new WeakReference(value); - set = true; - } - - if (!set) - { - CurrentValue = UnsetReference; - } + _accessor = accessor; + return Disposable.Create(() => _accessor = null); + }, + _ => accessor); } } } diff --git a/src/Markup/Avalonia.Markup/DefaultValueConverter.cs b/src/Markup/Avalonia.Markup/DefaultValueConverter.cs index 469b00d3ad..86d37d8e13 100644 --- a/src/Markup/Avalonia.Markup/DefaultValueConverter.cs +++ b/src/Markup/Avalonia.Markup/DefaultValueConverter.cs @@ -43,8 +43,18 @@ namespace Avalonia.Markup if (value != null) { - var message = $"Could not convert '{value}' to '{targetType}'"; - return new BindingError(new InvalidCastException(message)); + string message; + + if (TypeUtilities.IsNumeric(targetType)) + { + message = $"'{value}' is not a valid number."; + } + else + { + message = $"Could not convert '{value}' to '{targetType.Name}'."; + } + + return new BindingNotification(new InvalidCastException(message), BindingErrorType.Error); } return AvaloniaProperty.UnsetValue; diff --git a/src/Markup/Avalonia.Markup/IValueConverter.cs b/src/Markup/Avalonia.Markup/IValueConverter.cs index 23117a3fac..10d5c626c2 100644 --- a/src/Markup/Avalonia.Markup/IValueConverter.cs +++ b/src/Markup/Avalonia.Markup/IValueConverter.cs @@ -3,6 +3,7 @@ using System; using System.Globalization; +using Avalonia.Data; namespace Avalonia.Markup { @@ -21,8 +22,8 @@ namespace Avalonia.Markup /// The converted value. /// /// This method should not throw exceptions. If the value is not convertible, return - /// . Any exception thrown will be treated as - /// an application exception. + /// a in an error state. Any exceptions thrown will be + /// treated as an application exception. /// object Convert(object value, Type targetType, object parameter, CultureInfo culture); @@ -36,8 +37,8 @@ namespace Avalonia.Markup /// The converted value. /// /// This method should not throw exceptions. If the value is not convertible, return - /// . Any exception thrown will be treated as - /// an application exception. + /// a in an error state. Any exceptions thrown will be + /// treated as an application exception. /// object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture); } diff --git a/src/Markup/Avalonia.Markup/Properties/AssemblyInfo.cs b/src/Markup/Avalonia.Markup/Properties/AssemblyInfo.cs index d74ccf00a1..dd8c0a6bd3 100644 --- a/src/Markup/Avalonia.Markup/Properties/AssemblyInfo.cs +++ b/src/Markup/Avalonia.Markup/Properties/AssemblyInfo.cs @@ -8,4 +8,3 @@ using System.Runtime.CompilerServices; [assembly: AssemblyTitle("Avalonia.Markup")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Markup")] [assembly: InternalsVisibleTo("Avalonia.Markup.UnitTests")] -[assembly: InternalsVisibleTo("Avalonia.Markup.Xaml")] \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup/packages.config b/src/Markup/Avalonia.Markup/packages.config index 9f732f1bcb..bcef21429a 100644 --- a/src/Markup/Avalonia.Markup/packages.config +++ b/src/Markup/Avalonia.Markup/packages.config @@ -1,5 +1,6 @@  + diff --git a/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryContextImpl.cs index 5b8e8724d8..4228745d6b 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/StreamGeometryContextImpl.cs @@ -6,6 +6,8 @@ using Avalonia.Platform; using SharpDX.Direct2D1; using SweepDirection = SharpDX.Direct2D1.SweepDirection; using D2D = SharpDX.Direct2D1; +using Avalonia.Logging; +using System; namespace Avalonia.Direct2D1.Media { @@ -76,7 +78,20 @@ namespace Avalonia.Direct2D1.Media public void Dispose() { - _sink.Close(); + // Put a catch around sink.Close as it may throw if there were an error e.g. parsing a path. + try + { + _sink.Close(); + } + catch (Exception ex) + { + Logger.Error( + LogArea.Visual, + this, + "GeometrySink.Close exception: {Exception}", + ex); + } + _sink.Dispose(); } } diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj index 3fdde9339f..074fe84914 100644 --- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj +++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj @@ -38,6 +38,8 @@ CS1591 + + @@ -53,12 +55,19 @@ + + + Properties\SharedAssemblyInfo.cs + + Component + + diff --git a/src/Windows/Avalonia.Win32/Embedding/EmbeddedWindowImpl.cs b/src/Windows/Avalonia.Win32/Embedding/EmbeddedWindowImpl.cs index baa4cd4460..d27c28cb67 100644 --- a/src/Windows/Avalonia.Win32/Embedding/EmbeddedWindowImpl.cs +++ b/src/Windows/Avalonia.Win32/Embedding/EmbeddedWindowImpl.cs @@ -2,14 +2,17 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using Avalonia.Platform; using Avalonia.Win32.Interop; namespace Avalonia.Win32 { - public class EmbeddedWindowImpl : WindowImpl + class EmbeddedWindowImpl : WindowImpl, IEmbeddableWindowImpl { private static readonly System.Windows.Forms.UserControl WinFormsControl = new System.Windows.Forms.UserControl(); + public static IntPtr DefaultParentWindow = WinFormsControl.Handle; + protected override IntPtr CreateWindowOverride(ushort atom) { var hWnd = UnmanagedMethods.CreateWindowEx( @@ -21,11 +24,20 @@ namespace Avalonia.Win32 0, 640, 480, - WinFormsControl.Handle, + DefaultParentWindow, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); return hWnd; } + + protected override IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + if(msg == (uint)UnmanagedMethods.WindowsMessage.WM_KILLFOCUS) + LostFocus?.Invoke(); + return base.WndProc(hWnd, msg, wParam, lParam); + } + + public event Action LostFocus; } } diff --git a/src/Windows/Avalonia.Win32/Embedding/WinFormsAvaloniaControlHost.cs b/src/Windows/Avalonia.Win32/Embedding/WinFormsAvaloniaControlHost.cs new file mode 100644 index 0000000000..3b52090493 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Embedding/WinFormsAvaloniaControlHost.cs @@ -0,0 +1,91 @@ +using System; +using System.ComponentModel; +using System.Windows.Forms; +using Avalonia.Controls; +using Avalonia.Controls.Embedding; +using Avalonia.Input; +using Avalonia.VisualTree; +using Avalonia.Win32.Interop; +using WinFormsControl = System.Windows.Forms.Control; + +namespace Avalonia.Win32.Embedding +{ + [ToolboxItem(true)] + public class WinFormsAvaloniaControlHost : WinFormsControl + { + private readonly EmbeddableControlRoot _root = new EmbeddableControlRoot(); + + public WinFormsAvaloniaControlHost() + { + SetStyle(ControlStyles.AllPaintingInWmPaint, true); + UnmanagedMethods.SetParent(_root.PlatformImpl.Handle.Handle, Handle); + _root.Prepare(); + if (_root.IsFocused) + FocusManager.Instance.Focus(null); + _root.GotFocus += RootGotFocus; + _root.PlatformImpl.LostFocus += PlatformImpl_LostFocus; + FixPosition(); + } + + public Avalonia.Controls.Control Content + { + get { return (Avalonia.Controls.Control)_root.Content; } + set { _root.Content = value; } + } + + void Unfocus() + { + var focused = (IVisual)FocusManager.Instance.Current; + if (focused == null) + return; + while (focused.VisualParent != null) + focused = focused.VisualParent; + + if (focused == _root) + KeyboardDevice.Instance.SetFocusedElement(null, NavigationMethod.Unspecified, InputModifiers.None); + } + + private void PlatformImpl_LostFocus() + { + Unfocus(); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + _root.Dispose(); + base.Dispose(disposing); + } + + private void RootGotFocus(object sender, Interactivity.RoutedEventArgs e) + { + UnmanagedMethods.SetFocus(_root.PlatformImpl.Handle.Handle); + } + + protected override void OnGotFocus(EventArgs e) + { + if (_root != null) + UnmanagedMethods.SetFocus(_root.PlatformImpl.Handle.Handle); + } + + + void FixPosition() + { + if (_root != null && Width > 0 && Height > 0) + UnmanagedMethods.MoveWindow(_root.PlatformImpl.Handle.Handle, 0, 0, Width, Height, true); + } + + + + protected override void OnResize(EventArgs e) + { + FixPosition(); + base.OnResize(e); + } + + protected override void OnPaint(PaintEventArgs e) + { + + } + } +} diff --git a/src/Windows/Avalonia.Win32/Embedding/WpfAvaloniaControlHost.cs b/src/Windows/Avalonia.Win32/Embedding/WpfAvaloniaControlHost.cs new file mode 100644 index 0000000000..663f6906ed --- /dev/null +++ b/src/Windows/Avalonia.Win32/Embedding/WpfAvaloniaControlHost.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Forms.Integration; +using System.Windows.Interop; +using Avalonia.Controls; +using Avalonia.Win32.Interop; + +namespace Avalonia.Win32.Embedding +{ + public class WpfAvaloniaControlHost : HwndHost + { + private WinFormsAvaloniaControlHost _host; + private Avalonia.Controls.Control _content; + + public Avalonia.Controls.Control Content + { + get { return _content; } + set + { + if (_host != null) + _host.Content = value; + _content = value; + + } + } + + void DestroyHost() + { + _host?.Dispose(); + _host = null; + } + + protected override HandleRef BuildWindowCore(HandleRef hwndParent) + { + DestroyHost(); + _host = new WinFormsAvaloniaControlHost {Content = _content}; + UnmanagedMethods.SetParent(_host.Handle, hwndParent.Handle); + return new HandleRef(this, _host.Handle); + } + + protected override void DestroyWindowCore(HandleRef hwnd) + { + DestroyHost(); + } + } +} diff --git a/src/Windows/Avalonia.Win32/Input/KeyInterop.cs b/src/Windows/Avalonia.Win32/Input/KeyInterop.cs index 7f05a65095..11c0a6dca9 100644 --- a/src/Windows/Avalonia.Win32/Input/KeyInterop.cs +++ b/src/Windows/Avalonia.Win32/Input/KeyInterop.cs @@ -8,7 +8,7 @@ using Avalonia.Win32.Interop; namespace Avalonia.Win32.Input { - public static class KeyInterop + static class KeyInterop { private static readonly Dictionary s_virtualKeyFromKey = new Dictionary { diff --git a/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs b/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs index e241420d7d..fee1fe2ae6 100644 --- a/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs +++ b/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs @@ -8,7 +8,7 @@ using Avalonia.Win32.Interop; namespace Avalonia.Win32.Input { - public class WindowsKeyboardDevice : KeyboardDevice + class WindowsKeyboardDevice : KeyboardDevice { private readonly byte[] _keyStates = new byte[256]; diff --git a/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs b/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs index a521c5fc8c..a55c808415 100644 --- a/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs +++ b/src/Windows/Avalonia.Win32/Input/WindowsMouseDevice.cs @@ -8,7 +8,7 @@ using Avalonia.Win32.Interop; namespace Avalonia.Win32.Input { - public class WindowsMouseDevice : MouseDevice + class WindowsMouseDevice : MouseDevice { public new static WindowsMouseDevice Instance { get; } = new WindowsMouseDevice(); diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 8734bc9cac..ae030df49a 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -727,10 +727,13 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] public static extern IntPtr SetTimer(IntPtr hWnd, IntPtr nIDEvent, uint uElapse, TimerProc lpTimerFunc); - + [DllImport("user32.dll", SetLastError = true)] + public static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint); [DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, SetWindowPosFlags uFlags); [DllImport("user32.dll")] + public static extern bool SetFocus(IntPtr hWnd); + [DllImport("user32.dll")] public static extern bool SetParent(IntPtr hWnd, IntPtr hWndNewParent); [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, ShowWindowCommand nCmdShow); diff --git a/src/Windows/Avalonia.Win32/PlatformConstants.cs b/src/Windows/Avalonia.Win32/PlatformConstants.cs index 34114174e0..ba89c5bee6 100644 --- a/src/Windows/Avalonia.Win32/PlatformConstants.cs +++ b/src/Windows/Avalonia.Win32/PlatformConstants.cs @@ -3,7 +3,7 @@ namespace Avalonia.Win32 { - public static class PlatformConstants + static class PlatformConstants { public const string WindowHandleType = "HWND"; public const string CursorHandleType = "HCURSOR"; diff --git a/src/Windows/Avalonia.Win32/PopupImpl.cs b/src/Windows/Avalonia.Win32/PopupImpl.cs index ca2ba201d3..7849bd6a9d 100644 --- a/src/Windows/Avalonia.Win32/PopupImpl.cs +++ b/src/Windows/Avalonia.Win32/PopupImpl.cs @@ -7,7 +7,7 @@ using Avalonia.Win32.Interop; namespace Avalonia.Win32 { - public class PopupImpl : WindowImpl, IPopupImpl + class PopupImpl : WindowImpl, IPopupImpl { public override void Show() { diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index ac2e62d27f..a7597ffb86 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -31,7 +31,7 @@ namespace Avalonia namespace Avalonia.Win32 { - public class Win32Platform : IPlatformThreadingInterface, IPlatformSettings, IWindowingPlatform, IPlatformIconLoader + class Win32Platform : IPlatformThreadingInterface, IPlatformSettings, IWindowingPlatform, IPlatformIconLoader { private static readonly Win32Platform s_instance = new Win32Platform(); private static Thread _uiThread; @@ -177,7 +177,7 @@ namespace Avalonia.Win32 return new WindowImpl(); } - public IWindowImpl CreateEmbeddableWindow() + public IEmbeddableWindowImpl CreateEmbeddableWindow() { return new EmbeddedWindowImpl(); } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 0b09d9ec91..af68257357 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -18,7 +18,7 @@ using Avalonia.Win32.Interop; namespace Avalonia.Win32 { - public class WindowImpl : IWindowImpl + class WindowImpl : IWindowImpl { private static readonly List s_instances = new List(); diff --git a/src/iOS/Avalonia.iOS/WindowingPlatformImpl.cs b/src/iOS/Avalonia.iOS/WindowingPlatformImpl.cs index 72adb5c086..b6a683e10a 100644 --- a/src/iOS/Avalonia.iOS/WindowingPlatformImpl.cs +++ b/src/iOS/Avalonia.iOS/WindowingPlatformImpl.cs @@ -20,7 +20,7 @@ namespace Avalonia.iOS return _window; } - public IWindowImpl CreateEmbeddableWindow() + public IEmbeddableWindowImpl CreateEmbeddableWindow() { throw new NotImplementedException(); } diff --git a/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj b/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj index 6e4278fdfc..07ed7f14ca 100644 --- a/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj +++ b/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj @@ -90,6 +90,7 @@ + diff --git a/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.v2.ncrunchproject b/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.v2.ncrunchproject index 30815b1937..b5cd70b13f 100644 --- a/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.v2.ncrunchproject +++ b/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.v2.ncrunchproject @@ -17,10 +17,11 @@ true true 60000 - - - + + + AutoDetect STA x86 + LongTestTimesWithoutParallelExecution \ No newline at end of file diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index 2e7db2da05..66fe3c7767 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -54,6 +54,36 @@ namespace Avalonia.Base.UnitTests Assert.False(target.IsSet(Class1.QuxProperty)); } + [Fact] + public void OneTime_Binding_Ignores_UnsetValue() + { + var target = new Class1(); + var source = new Subject(); + + target.Bind(Class1.QuxProperty, new TestOneTimeBinding(source)); + + source.OnNext(AvaloniaProperty.UnsetValue); + Assert.Equal(5.6, target.GetValue(Class1.QuxProperty)); + + source.OnNext(6.7); + Assert.Equal(6.7, target.GetValue(Class1.QuxProperty)); + } + + [Fact] + public void OneTime_Binding_Ignores_Binding_Errors() + { + var target = new Class1(); + var source = new Subject(); + + target.Bind(Class1.QuxProperty, new TestOneTimeBinding(source)); + + source.OnNext(new BindingNotification(new Exception(), BindingErrorType.Error)); + Assert.Equal(5.6, target.GetValue(Class1.QuxProperty)); + + source.OnNext(6.7); + Assert.Equal(6.7, target.GetValue(Class1.QuxProperty)); + } + [Fact] public void Bind_Throws_Exception_For_Unregistered_Property() { @@ -273,31 +303,36 @@ namespace Avalonia.Base.UnitTests target.Bind(Class1.QuxProperty, source); source.OnNext(6.7); - source.OnNext(new BindingError(new InvalidOperationException("Foo"))); + source.OnNext(new BindingNotification( + new InvalidOperationException("Foo"), + BindingErrorType.Error)); Assert.Equal(6.7, target.GetValue(Class1.QuxProperty)); } [Fact] - public void BindingError_With_FallbackValue_Causes_Target_Update() + public void BindingNotification_With_FallbackValue_Causes_Target_Update() { var target = new Class1(); var source = new Subject(); target.Bind(Class1.QuxProperty, source); source.OnNext(6.7); - source.OnNext(new BindingError(new InvalidOperationException("Foo"), 8.9)); + source.OnNext(new BindingNotification( + new InvalidOperationException("Foo"), + BindingErrorType.Error, + 8.9)); Assert.Equal(8.9, target.GetValue(Class1.QuxProperty)); } [Fact] - public void Bind_Logs_BindingError() + public void Bind_Logs_Binding_Error() { var target = new Class1(); var source = new Subject(); var called = false; - var expectedMessageTemplate = "Error binding to {Target}.{Property}: {Message}"; + var expectedMessageTemplate = "Error in binding to {Target}.{Property}: {Message}"; LogCallback checkLogMessage = (level, area, src, mt, pv) => { @@ -313,7 +348,9 @@ namespace Avalonia.Base.UnitTests { target.Bind(Class1.QuxProperty, source); source.OnNext(6.7); - source.OnNext(new BindingError(new InvalidOperationException("Foo"))); + source.OnNext(new BindingNotification( + new InvalidOperationException("Foo"), + BindingErrorType.Error)); Assert.Equal(6.7, target.GetValue(Class1.QuxProperty)); Assert.True(called); @@ -345,5 +382,24 @@ namespace Avalonia.Base.UnitTests public static readonly StyledProperty BarProperty = AvaloniaProperty.Register("Bar", "bardefault"); } + + private class TestOneTimeBinding : IBinding + { + private IObservable _source; + + public TestOneTimeBinding(IObservable source) + { + _source = source; + } + + public InstancedBinding Initiate( + IAvaloniaObject target, + AvaloniaProperty targetProperty, + object anchor = null, + bool enableDataValidation = false) + { + return new InstancedBinding(_source, BindingMode.OneTime); + } + } } } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs new file mode 100644 index 0000000000..c8436c376f --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Subjects; +using Avalonia.Data; +using Xunit; + +namespace Avalonia.Base.UnitTests +{ + public class AvaloniaObjectTests_DataValidation + { + [Fact] + public void Setting_Non_Validated_Property_Does_Not_Call_UpdateDataValidation() + { + var target = new Class1(); + + target.SetValue(Class1.NonValidatedDirectProperty, 6); + + Assert.Empty(target.Notifications); + } + + [Fact] + public void Setting_Non_Validated_Direct_Property_Does_Not_Call_UpdateDataValidation() + { + var target = new Class1(); + + target.SetValue(Class1.NonValidatedDirectProperty, 6); + + Assert.Empty(target.Notifications); + } + + [Fact] + public void Setting_Validated_Direct_Property_Calls_UpdateDataValidation() + { + var target = new Class1(); + + target.SetValue(Class1.ValidatedDirectProperty, new BindingNotification(6)); + target.SetValue(Class1.ValidatedDirectProperty, new BindingNotification(new Exception(), BindingErrorType.Error)); + target.SetValue(Class1.ValidatedDirectProperty, new BindingNotification(new Exception(), BindingErrorType.DataValidationError)); + target.SetValue(Class1.ValidatedDirectProperty, new BindingNotification(7)); + + Assert.Equal( + new[] + { + new BindingNotification(6), + new BindingNotification(new Exception(), BindingErrorType.Error), + new BindingNotification(new Exception(), BindingErrorType.DataValidationError), + new BindingNotification(7), + }, + target.Notifications.AsEnumerable()); + } + + [Fact] + public void Binding_Non_Validated_Property_Does_Not_Call_UpdateDataValidation() + { + var source = new Subject(); + var target = new Class1 + { + [!Class1.NonValidatedProperty] = source.AsBinding(), + }; + + source.OnNext(new BindingNotification(6)); + source.OnNext(new BindingNotification(new Exception(), BindingErrorType.Error)); + source.OnNext(new BindingNotification(new Exception(), BindingErrorType.DataValidationError)); + source.OnNext(new BindingNotification(7)); + + Assert.Empty(target.Notifications); + } + + [Fact] + public void Binding_Validated_Direct_Property_Calls_UpdateDataValidation() + { + var source = new Subject(); + var target = new Class1 + { + [!Class1.ValidatedDirectProperty] = source.AsBinding(), + }; + + source.OnNext(new BindingNotification(6)); + source.OnNext(new BindingNotification(new Exception(), BindingErrorType.Error)); + source.OnNext(new BindingNotification(new Exception(), BindingErrorType.DataValidationError)); + source.OnNext(new BindingNotification(7)); + + Assert.Equal( + new[] + { + new BindingNotification(6), + new BindingNotification(new Exception(), BindingErrorType.Error), + new BindingNotification(new Exception(), BindingErrorType.DataValidationError), + new BindingNotification(7), + }, + target.Notifications.AsEnumerable()); + } + + private class Class1 : AvaloniaObject + { + public static readonly StyledProperty NonValidatedProperty = + AvaloniaProperty.Register( + nameof(NonValidated)); + + public static readonly DirectProperty NonValidatedDirectProperty = + AvaloniaProperty.RegisterDirect( + nameof(NonValidatedDirect), + o => o.NonValidatedDirect, + (o, v) => o.NonValidatedDirect = v); + + public static readonly DirectProperty ValidatedDirectProperty = + AvaloniaProperty.RegisterDirect( + nameof(ValidatedDirect), + o => o.ValidatedDirect, + (o, v) => o.ValidatedDirect = v, + enableDataValidation: true); + + private int _nonValidatedDirect; + private int _direct; + + public int NonValidated + { + get { return GetValue(NonValidatedProperty); } + set { SetValue(NonValidatedProperty, value); } + } + + public int NonValidatedDirect + { + get { return _direct; } + set { SetAndRaise(NonValidatedDirectProperty, ref _nonValidatedDirect, value); } + } + + public int ValidatedDirect + { + get { return _direct; } + set { SetAndRaise(ValidatedDirectProperty, ref _direct, value); } + } + + public IList Notifications { get; } = new List(); + + protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification notification) + { + Notifications.Add(notification); + } + } + } +} diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs index 1d8bb80cba..cbeb8765c3 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs @@ -284,7 +284,6 @@ namespace Avalonia.Base.UnitTests Assert.Equal("newvalue", target.Foo); } - [Fact] public void UnsetValue_Is_Used_On_AddOwnered_Property() { @@ -360,7 +359,7 @@ namespace Avalonia.Base.UnitTests target.Bind(Class1.FooProperty, source); source.OnNext("initial"); - source.OnNext(new BindingError(new InvalidOperationException("Foo"))); + source.OnNext(new BindingNotification(new InvalidOperationException("Foo"), BindingErrorType.Error)); Assert.Equal("initial", target.GetValue(Class1.FooProperty)); } @@ -373,7 +372,10 @@ namespace Avalonia.Base.UnitTests target.Bind(Class1.FooProperty, source); source.OnNext("initial"); - source.OnNext(new BindingError(new InvalidOperationException("Foo"), "fallback")); + source.OnNext(new BindingNotification( + new InvalidOperationException("Foo"), + BindingErrorType.Error, + "fallback")); Assert.Equal("fallback", target.GetValue(Class1.FooProperty)); } @@ -389,7 +391,7 @@ namespace Avalonia.Base.UnitTests { if (level == LogEventLevel.Error && area == LogArea.Binding && - mt == "Error binding to {Target}.{Property}: {Message}" && + mt == "Error in binding to {Target}.{Property}: {Message}" && pv.Length == 3 && pv[0] is Class1 && object.ReferenceEquals(pv[1], Class1.FooProperty) && @@ -403,7 +405,7 @@ namespace Avalonia.Base.UnitTests { target.Bind(Class1.FooProperty, source); source.OnNext("baz"); - source.OnNext(new BindingError(new InvalidOperationException("Binding Error Message"))); + source.OnNext(new BindingNotification(new InvalidOperationException("Binding Error Message"), BindingErrorType.Error)); } Assert.True(called); diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs index 4c15f9e676..da0b0252a3 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs @@ -25,7 +25,7 @@ namespace Avalonia.Base.UnitTests .Select(x => x.Name) .ToArray(); - Assert.Equal(new[] { "Foo", "Baz", "Qux", "Attached", "ValidationStatus" }, names); + Assert.Equal(new[] { "Foo", "Baz", "Qux", "Attached" }, names); } [Fact] @@ -35,7 +35,7 @@ namespace Avalonia.Base.UnitTests .Select(x => x.Name) .ToArray(); - Assert.Equal(new[] { "Bar", "Flob", "Fred", "Foo", "Baz", "Qux", "Attached", "ValidationStatus" }, names); + Assert.Equal(new[] { "Bar", "Flob", "Fred", "Foo", "Baz", "Qux", "Attached" }, names); } [Fact] diff --git a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj index ef831b27fd..e46b43d90e 100644 --- a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj +++ b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj @@ -98,7 +98,7 @@ - + diff --git a/tests/Avalonia.Controls.UnitTests/DropDownTests.cs b/tests/Avalonia.Controls.UnitTests/DropDownTests.cs index fc866e5f86..30cca90b4a 100644 --- a/tests/Avalonia.Controls.UnitTests/DropDownTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DropDownTests.cs @@ -1,20 +1,65 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; using System.Linq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; +using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; -using Avalonia.Platform; -using Ploeh.AutoFixture; -using Ploeh.AutoFixture.AutoMoq; +using Avalonia.LogicalTree; +using Avalonia.Media; +using Avalonia.UnitTests; +using Avalonia.VisualTree; using Xunit; namespace Avalonia.Controls.UnitTests { public class DropDownTests { + [Fact] + public void SelectionBoxItem_Is_Rectangle_With_VisualBrush_When_Selection_Is_Control() + { + var items = new[] { new Canvas() }; + var target = new DropDown + { + Items = items, + SelectedIndex = 0, + }; + + var rectangle = target.GetValue(DropDown.SelectionBoxItemProperty) as Rectangle; + Assert.NotNull(rectangle); + + var brush = rectangle.Fill as VisualBrush; + Assert.NotNull(brush); + Assert.Same(items[0], brush.Visual); + } + + [Fact] + public void SelectionBoxItem_Rectangle_Is_Removed_From_Logical_Tree() + { + var target = new DropDown + { + Items = new[] { new Canvas() }, + SelectedIndex = 0, + Template = GetTemplate(), + }; + + var root = new TestRoot { Child = target }; + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + var rectangle = target.GetValue(DropDown.SelectionBoxItemProperty) as Rectangle; + Assert.True(((ILogical)target).IsAttachedToLogicalTree); + Assert.True(((ILogical)rectangle).IsAttachedToLogicalTree); + + rectangle.DetachedFromLogicalTree += (s, e) => { }; + + root.Child = null; + + Assert.False(((ILogical)target).IsAttachedToLogicalTree); + Assert.False(((ILogical)rectangle).IsAttachedToLogicalTree); + } + private FuncControlTemplate GetTemplate() { return new FuncControlTemplate(parent => @@ -26,8 +71,7 @@ namespace Avalonia.Controls.UnitTests { new ContentControl { - Name = "contentControl", - [~ContentPresenter.ContentProperty] = parent[~DropDown.SelectionBoxItemProperty], + [!ContentControl.ContentProperty] = parent[!DropDown.SelectionBoxItemProperty], }, new ToggleButton { @@ -35,7 +79,12 @@ namespace Avalonia.Controls.UnitTests }, new Popup { - Name = "popup", + Name = "PART_Popup", + Child = new ItemsPresenter + { + Name = "PART_ItemsPresenter", + [!ItemsPresenter.ItemsProperty] = parent[!DropDown.ItemsProperty], + } } } }; diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 5581087b5f..13d97920ee 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -166,6 +166,24 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + [Fact] + public void PopupRoot_Should_Be_Detached_From_Logical_Tree_When_Popup_Is_Detached() + { + using (CreateServices()) + { + var target = new Popup(); + var root = new TestRoot { Child = target }; + + target.Open(); + + var popupRoot = (ILogical)target.PopupRoot; + + Assert.True(popupRoot.IsAttachedToLogicalTree); + root.Child = null; + Assert.False(((ILogical)target).IsAttachedToLogicalTree); + } + } + [Fact] public void PopupRoot_Should_Have_Template_Applied() { diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 5619311537..8f206cc016 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -1,8 +1,11 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; +using Avalonia.Markup.Xaml.Data; using Avalonia.Platform; using Avalonia.UnitTests; using Moq; @@ -21,6 +24,52 @@ namespace Avalonia.Controls.UnitTests } [Fact] + public void Typing_Beginning_With_0_Should_Not_Modify_Text_When_Bound_To_Int() + { + using (UnitTestApplication.Start(Services)) + { + var source = new Class1(); + var target = new TextBox + { + DataContext = source, + Template = CreateTemplate(), + }; + + target.ApplyTemplate(); + target.Bind(TextBox.TextProperty, new Binding(nameof(Class1.Foo), BindingMode.TwoWay)); + + Assert.Equal("0", target.Text); + + target.CaretIndex = 1; + target.RaiseEvent(new TextInputEventArgs + { + RoutedEvent = InputElement.TextInputEvent, + Text = "2", + }); + + Assert.Equal("02", target.Text); + } + } + + private static TestServices Services => TestServices.MockThreadingInterface.With( + standardCursorFactory: Mock.Of()); + + private IControlTemplate CreateTemplate() + { + return new FuncControlTemplate(control => + new TextPresenter + { + Name = "PART_TextPresenter", + [!!TextPresenter.TextProperty] = new Binding + { + Path = "Text", + Mode = BindingMode.TwoWay, + Priority = BindingPriority.TemplatedParent, + RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent), + }, + }); + } + public void Control_Backspace_Should_Remove_The_Word_Before_The_Caret_If_There_Is_No_Selection() { AvaloniaLocator.CurrentMutable @@ -109,5 +158,16 @@ namespace Avalonia.Controls.UnitTests Key = key }); } + + private class Class1 : NotifyingBase + { + private int _foo; + + public int Foo + { + get { return _foo; } + set { _foo = value; RaisePropertyChanged(); } + } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests_ValidationState.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs similarity index 51% rename from tests/Avalonia.Controls.UnitTests/TextBoxTests_ValidationState.cs rename to tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs index 6ff3629acf..ff3634b9fe 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests_ValidationState.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs @@ -5,71 +5,84 @@ using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; +using System.Linq; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.Markup.Xaml.Data; +using Avalonia.Platform; using Avalonia.UnitTests; +using Moq; using Xunit; namespace Avalonia.Controls.UnitTests { - public class TextBoxTests_ValidationState + public class TextBoxTests_DataValidation { [Fact] - public void Setter_Exceptions_Should_Set_ValidationState() + public void Setter_Exceptions_Should_Set_Error_Pseudoclass() { - using (UnitTestApplication.Start(TestServices.MockThreadingInterface)) + using (UnitTestApplication.Start(Services)) { - var target = new TextBox(); - var binding = new Binding(nameof(ExceptionTest.LessThan10)); - binding.Source = new ExceptionTest(); - binding.EnableValidation = true; - target.Bind(TextBox.TextProperty, binding); + var target = new TextBox + { + DataContext = new ExceptionTest(), + [!TextBox.TextProperty] = new Binding(nameof(ExceptionTest.LessThan10), BindingMode.TwoWay), + Template = CreateTemplate(), + }; - Assert.True(target.ValidationStatus.IsValid); - target.Text = "20"; - Assert.False(target.ValidationStatus.IsValid); - target.Text = "1"; - Assert.True(target.ValidationStatus.IsValid); - } - } + target.ApplyTemplate(); - [Fact(Skip = "TODO: Not yet passing")] - public void Unconvertable_Value_Should_Set_ValidationState() - { - using (UnitTestApplication.Start(TestServices.MockThreadingInterface)) - { - var target = new TextBox(); - var binding = new Binding(nameof(ExceptionTest.LessThan10)); - binding.Source = new ExceptionTest(); - binding.EnableValidation = true; - target.Bind(TextBox.TextProperty, binding); - - Assert.True(target.ValidationStatus.IsValid); - target.Text = "foo"; - Assert.False(target.ValidationStatus.IsValid); + Assert.False(target.Classes.Contains(":error")); + target.Text = "20"; + Assert.True(target.Classes.Contains(":error")); target.Text = "1"; - Assert.True(target.ValidationStatus.IsValid); + Assert.False(target.Classes.Contains(":error")); } } [Fact] - public void Indei_Should_Set_ValidationState() + public void Setter_Exceptions_Should_Set_DataValidationErrors() { - using (UnitTestApplication.Start(TestServices.MockThreadingInterface)) + using (UnitTestApplication.Start(Services)) { - var target = new TextBox(); - var binding = new Binding(nameof(ExceptionTest.LessThan10)); - binding.Source = new IndeiTest(); - binding.EnableValidation = true; - target.Bind(TextBox.TextProperty, binding); + var target = new TextBox + { + DataContext = new ExceptionTest(), + [!TextBox.TextProperty] = new Binding(nameof(ExceptionTest.LessThan10), BindingMode.TwoWay), + Template = CreateTemplate(), + }; + + target.ApplyTemplate(); - Assert.True(target.ValidationStatus.IsValid); + Assert.Null(target.DataValidationErrors); target.Text = "20"; - Assert.False(target.ValidationStatus.IsValid); + Assert.Equal(1, target.DataValidationErrors.Count()); + Assert.IsType(target.DataValidationErrors.Single()); target.Text = "1"; - Assert.True(target.ValidationStatus.IsValid); + Assert.Null(target.DataValidationErrors); } } + private static TestServices Services => TestServices.MockThreadingInterface.With( + standardCursorFactory: Mock.Of()); + + private IControlTemplate CreateTemplate() + { + return new FuncControlTemplate(control => + new TextPresenter + { + Name = "PART_TextPresenter", + [!!TextPresenter.TextProperty] = new Binding + { + Path = "Text", + Mode = BindingMode.TwoWay, + Priority = BindingPriority.TemplatedParent, + RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent), + }, + }); + } + private class ExceptionTest { private int _lessThan10; diff --git a/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs b/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs index a97e4a2140..a9c991b8b9 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs @@ -20,7 +20,7 @@ namespace Avalonia.Controls.UnitTests return _windowImpl?.Invoke() ?? Mock.Of(x => x.Scaling == 1); } - public IWindowImpl CreateEmbeddableWindow() + public IEmbeddableWindowImpl CreateEmbeddableWindow() { throw new NotImplementedException(); } diff --git a/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj b/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj index 587deb4864..9aef304beb 100644 --- a/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj +++ b/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj @@ -98,6 +98,7 @@ + diff --git a/tests/Avalonia.LeakTests/ExpressionObserverTests.cs b/tests/Avalonia.LeakTests/ExpressionObserverTests.cs new file mode 100644 index 0000000000..3dbc62424f --- /dev/null +++ b/tests/Avalonia.LeakTests/ExpressionObserverTests.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using Avalonia.Collections; +using Avalonia.Markup.Data; +using Avalonia.UnitTests; +using JetBrains.dotMemoryUnit; +using Xunit; +using Xunit.Abstractions; + +namespace Avalonia.LeakTests +{ + [DotMemoryUnit(FailIfRunWithoutSupport = false)] + public class ExpressionObserverTests + { + public ExpressionObserverTests(ITestOutputHelper atr) + { + DotMemoryUnitTestOutput.SetOutputMethod(atr.WriteLine); + } + + [Fact] + public void Should_Not_Keep_Source_Alive_ObservableCollection() + { + Func run = () => + { + var source = new { Foo = new AvaloniaList {"foo", "bar"} }; + var target = new ExpressionObserver(source, "Foo"); + + target.Subscribe(_ => { }); + return target; + }; + + var result = run(); + + dotMemory.Check(memory => + Assert.Equal(0, memory.GetObjects(where => where.Type.Is>()).ObjectsCount)); + } + + [Fact] + public void Should_Not_Keep_Source_Alive_ObservableCollection_With_DataValidation() + { + Func run = () => + { + var source = new { Foo = new AvaloniaList { "foo", "bar" } }; + var target = new ExpressionObserver(source, "Foo", true); + + target.Subscribe(_ => { }); + return target; + }; + + var result = run(); + + dotMemory.Check(memory => + Assert.Equal(0, memory.GetObjects(where => where.Type.Is>()).ObjectsCount)); + } + + [Fact] + public void Should_Not_Keep_Source_Alive_NonIntegerIndexer() + { + Func run = () => + { + var source = new { Foo = new NonIntegerIndexer() }; + var target = new ExpressionObserver(source, "Foo"); + + target.Subscribe(_ => { }); + return target; + }; + + var result = run(); + + dotMemory.Check(memory => + Assert.Equal(0, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); + } + + private class NonIntegerIndexer : NotifyingBase + { + private readonly Dictionary _storage = new Dictionary(); + + public string this[string key] + { + get + { + return _storage[key]; + } + set + { + _storage[key] = value; + RaisePropertyChanged(CommonPropertyNames.IndexerName); + } + } + } + } +} diff --git a/tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj b/tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj index ae691bfc0f..66d234ac19 100644 --- a/tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj +++ b/tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj @@ -10,10 +10,11 @@ Properties Avalonia.Markup.UnitTests Avalonia.Markup.UnitTests - v4.6 + v4.5 512 + true @@ -34,7 +35,7 @@ - ..\..\packages\Microsoft.Reactive.Testing.3.0.0\lib\net46\Microsoft.Reactive.Testing.dll + ..\..\packages\Microsoft.Reactive.Testing.3.0.0\lib\net45\Microsoft.Reactive.Testing.dll True @@ -43,9 +44,13 @@ True + + C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETPortable\v4.5\Profile\Profile7\System.ComponentModel.Annotations.dll + + - ..\..\packages\System.Reactive.Core.3.0.0\lib\net46\System.Reactive.Core.dll + ..\..\packages\System.Reactive.Core.3.0.0\lib\net45\System.Reactive.Core.dll True @@ -53,11 +58,15 @@ True - ..\..\packages\System.Reactive.Linq.3.0.0\lib\net46\System.Reactive.Linq.dll + ..\..\packages\System.Reactive.Linq.3.0.0\lib\net45\System.Reactive.Linq.dll True - ..\..\packages\System.Reactive.PlatformServices.3.0.0\lib\net46\System.Reactive.PlatformServices.dll + ..\..\packages\System.Reactive.PlatformServices.3.0.0\lib\net45\System.Reactive.PlatformServices.dll + True + + + ..\..\packages\System.Reactive.Windows.Threading.3.0.0\lib\net45\System.Reactive.Windows.Threading.dll True @@ -66,6 +75,7 @@ + ..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll True @@ -85,7 +95,10 @@ - + + + + @@ -97,9 +110,8 @@ - - - + + diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs new file mode 100644 index 0000000000..c53dc417b0 --- /dev/null +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingExpressionTests.cs @@ -0,0 +1,320 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Reactive.Linq; +using System.Threading; +using Avalonia.Data; +using Avalonia.Markup.Data; +using Avalonia.UnitTests; +using Moq; +using Xunit; + +namespace Avalonia.Markup.UnitTests.Data +{ + public class BindingExpressionTests + { + [Fact] + public async void Should_Get_Simple_Property_Value() + { + var data = new Class1 { StringValue = "foo" }; + var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(string)); + var result = await target.Take(1); + + Assert.Equal("foo", result); + } + + [Fact] + public void Should_Set_Simple_Property_Value() + { + var data = new Class1 { StringValue = "foo" }; + var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(string)); + + target.OnNext("bar"); + + Assert.Equal("bar", data.StringValue); + } + + [Fact] + public async void Should_Convert_Get_String_To_Double() + { + Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; + + var data = new Class1 { StringValue = "5.6" }; + var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double)); + var result = await target.Take(1); + + Assert.Equal(5.6, result); + } + + [Fact] + public async void Getting_Invalid_Double_String_Should_Return_BindingError() + { + var data = new Class1 { StringValue = "foo" }; + var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double)); + var result = await target.Take(1); + + Assert.IsType(result); + } + + [Fact] + public async void Should_Coerce_Get_Null_Double_String_To_UnsetValue() + { + var data = new Class1 { StringValue = null }; + var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double)); + var result = await target.Take(1); + + Assert.Equal(AvaloniaProperty.UnsetValue, result); + } + + [Fact] + public void Should_Convert_Set_String_To_Double() + { + Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; + + var data = new Class1 { StringValue = (5.6).ToString() }; + var target = new BindingExpression(new ExpressionObserver(data, "StringValue"), typeof(double)); + + target.OnNext(6.7); + + Assert.Equal((6.7).ToString(), data.StringValue); + } + + [Fact] + public async void Should_Convert_Get_Double_To_String() + { + Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; + + var data = new Class1 { DoubleValue = 5.6 }; + var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string)); + var result = await target.Take(1); + + Assert.Equal((5.6).ToString(), result); + } + + [Fact] + public void Should_Convert_Set_Double_To_String() + { + Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; + + var data = new Class1 { DoubleValue = 5.6 }; + var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string)); + + target.OnNext("6.7"); + + Assert.Equal(6.7, data.DoubleValue); + } + + [Fact] + public async void Should_Return_BindingNotification_With_FallbackValue_For_NonConvertibe_Target_Value() + { + Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; + + var data = new Class1 { StringValue = "foo" }; + var target = new BindingExpression( + new ExpressionObserver(data, "StringValue"), + typeof(int), + 42, + DefaultValueConverter.Instance); + var result = await target.Take(1); + + Assert.Equal( + new BindingNotification( + new InvalidCastException("'foo' is not a valid number."), + BindingErrorType.Error, + 42), + result); + } + + [Fact] + public async void Should_Return_BindingNotification_With_FallbackValue_For_NonConvertibe_Target_Value_With_Data_Validation() + { + Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; + + var data = new Class1 { StringValue = "foo" }; + var target = new BindingExpression( + new ExpressionObserver(data, "StringValue", true), + typeof(int), + 42, + DefaultValueConverter.Instance); + var result = await target.Take(1); + + Assert.Equal( + new BindingNotification( + new InvalidCastException("'foo' is not a valid number."), + BindingErrorType.Error, + 42), + result); + } + + [Fact] + public async void Should_Return_BindingNotification_For_Invalid_FallbackValue() + { + Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; + + var data = new Class1 { StringValue = "foo" }; + var target = new BindingExpression( + new ExpressionObserver(data, "StringValue"), + typeof(int), + "bar", + DefaultValueConverter.Instance); + var result = await target.Take(1); + + Assert.Equal( + new BindingNotification( + new AggregateException( + new InvalidCastException("Could not convert 'foo' to 'System.Int32'"), + new InvalidCastException("Could not convert FallbackValue 'bar' to 'System.Int32'")), + BindingErrorType.Error), + result); + } + + [Fact] + public async void Should_Return_BindingNotification_For_Invalid_FallbackValue_With_Data_Validation() + { + Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; + + var data = new Class1 { StringValue = "foo" }; + var target = new BindingExpression( + new ExpressionObserver(data, "StringValue", true), + typeof(int), + "bar", + DefaultValueConverter.Instance); + var result = await target.Take(1); + + Assert.Equal( + new BindingNotification( + new AggregateException( + new InvalidCastException("Could not convert 'foo' to 'System.Int32'"), + new InvalidCastException("Could not convert FallbackValue 'bar' to 'System.Int32'")), + BindingErrorType.Error), + result); + } + + [Fact] + public void Setting_Invalid_Double_String_Should_Not_Change_Target() + { + var data = new Class1 { DoubleValue = 5.6 }; + var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string)); + + target.OnNext("foo"); + + Assert.Equal(5.6, data.DoubleValue); + } + + [Fact] + public void Setting_Invalid_Double_String_Should_Use_FallbackValue() + { + var data = new Class1 { DoubleValue = 5.6 }; + var target = new BindingExpression( + new ExpressionObserver(data, "DoubleValue"), + typeof(string), + "9.8", + DefaultValueConverter.Instance); + + target.OnNext("foo"); + + Assert.Equal(9.8, data.DoubleValue); + } + + [Fact] + public void Should_Coerce_Setting_Null_Double_To_Default_Value() + { + var data = new Class1 { DoubleValue = 5.6 }; + var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string)); + + target.OnNext(null); + + Assert.Equal(0, data.DoubleValue); + } + + [Fact] + public void Should_Coerce_Setting_UnsetValue_Double_To_Default_Value() + { + var data = new Class1 { DoubleValue = 5.6 }; + var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue"), typeof(string)); + + target.OnNext(AvaloniaProperty.UnsetValue); + + Assert.Equal(0, data.DoubleValue); + } + + [Fact] + public void Should_Pass_ConverterParameter_To_Convert() + { + var data = new Class1 { DoubleValue = 5.6 }; + var converter = new Mock(); + var target = new BindingExpression( + new ExpressionObserver(data, "DoubleValue"), + typeof(string), + converter.Object, + converterParameter: "foo"); + + target.Subscribe(_ => { }); + + converter.Verify(x => x.Convert(5.6, typeof(string), "foo", CultureInfo.CurrentUICulture)); + } + + [Fact] + public void Should_Pass_ConverterParameter_To_ConvertBack() + { + var data = new Class1 { DoubleValue = 5.6 }; + var converter = new Mock(); + var target = new BindingExpression( + new ExpressionObserver(data, "DoubleValue"), + typeof(string), + converter.Object, + converterParameter: "foo"); + + target.OnNext("bar"); + + converter.Verify(x => x.ConvertBack("bar", typeof(double), "foo", CultureInfo.CurrentUICulture)); + } + + [Fact] + public void Should_Handle_DataValidation() + { + var data = new Class1 { DoubleValue = 5.6 }; + var converter = new Mock(); + var target = new BindingExpression(new ExpressionObserver(data, "DoubleValue", true), typeof(string)); + var result = new List(); + + target.Subscribe(x => result.Add(x)); + target.OnNext(1.2); + target.OnNext("3.4"); + target.OnNext("bar"); + + Assert.Equal( + new[] + { + new BindingNotification("5.6"), + new BindingNotification("1.2"), + new BindingNotification("3.4"), + new BindingNotification( + new InvalidCastException("'bar' is not a valid number."), + BindingErrorType.Error) + }, + result); + } + + private class Class1 : NotifyingBase + { + private string _stringValue; + private double _doubleValue; + + public string StringValue + { + get { return _stringValue; } + set { _stringValue = value; RaisePropertyChanged(); } + } + + public double DoubleValue + { + get { return _doubleValue; } + set { _doubleValue = value; RaisePropertyChanged(); } + } + } + } +} diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExceptionValidatorTests.cs b/tests/Avalonia.Markup.UnitTests/Data/ExceptionValidatorTests.cs deleted file mode 100644 index 6ff336c5a6..0000000000 --- a/tests/Avalonia.Markup.UnitTests/Data/ExceptionValidatorTests.cs +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.ComponentModel; -using System.Runtime.CompilerServices; -using Avalonia.Data; -using Avalonia.Markup.Data.Plugins; -using Xunit; - -namespace Avalonia.Markup.UnitTests.Data -{ - public class ExceptionValidatorTests - { - public class Data : INotifyPropertyChanged - { - private int nonValidated; - - public int NonValidated - { - get { return nonValidated; } - set { nonValidated = value; NotifyPropertyChanged(); } - } - - private int mustBePositive; - - public int MustBePositive - { - get { return mustBePositive; } - set - { - if (value <= 0) - { - throw new ArgumentOutOfRangeException(nameof(value)); - } - mustBePositive = value; - } - } - - public event PropertyChangedEventHandler PropertyChanged; - - private void NotifyPropertyChanged([CallerMemberName] string propertyName = "") - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - } - - [Fact] - public void Setting_Non_Validating_Triggers_Validation() - { - var inpcAccessorPlugin = new InpcPropertyAccessorPlugin(); - var validatorPlugin = new ExceptionValidationPlugin(); - var data = new Data(); - var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.NonValidated), _ => { }); - IValidationStatus status = null; - var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.NonValidated), accessor, s => status = s); - - validator.SetValue(5, BindingPriority.LocalValue); - - Assert.NotNull(status); - } - - [Fact] - public void Setting_Validating_Property_To_Valid_Value_Returns_Successful_ValidationStatus() - { - var inpcAccessorPlugin = new InpcPropertyAccessorPlugin(); - var validatorPlugin = new ExceptionValidationPlugin(); - var data = new Data(); - var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), _ => { }); - IValidationStatus status = null; - var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor, s => status = s); - - validator.SetValue(5, BindingPriority.LocalValue); - - Assert.True(status.IsValid); - } - - [Fact] - public void Setting_Validating_Property_To_Invalid_Value_Returns_Failed_ValidationStatus() - { - var inpcAccessorPlugin = new InpcPropertyAccessorPlugin(); - var validatorPlugin = new ExceptionValidationPlugin(); - var data = new Data(); - var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), _ => { }); - IValidationStatus status = null; - var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor, s => status = s); - - validator.SetValue(-5, BindingPriority.LocalValue); - - Assert.False(status.IsValid); - } - } -} diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_DataValidation.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_DataValidation.cs new file mode 100644 index 0000000000..fb98144647 --- /dev/null +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_DataValidation.cs @@ -0,0 +1,222 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; +using Avalonia.Data; +using Avalonia.Markup.Data; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Markup.UnitTests.Data +{ + public class ExpressionObserverTests_DataValidation + { + [Fact] + public void Doesnt_Send_DataValidationError_When_DataValidatation_Not_Enabled() + { + var data = new ExceptionTest { MustBePositive = 5 }; + var observer = new ExpressionObserver(data, nameof(data.MustBePositive), false); + var validationMessageFound = false; + + observer.OfType() + .Where(x => x.ErrorType == BindingErrorType.DataValidationError) + .Subscribe(_ => validationMessageFound = true); + observer.SetValue(-5); + + Assert.False(validationMessageFound); + } + + [Fact] + public void Exception_Validation_Sends_DataValidationError() + { + var data = new ExceptionTest { MustBePositive = 5 }; + var observer = new ExpressionObserver(data, nameof(data.MustBePositive), true); + var validationMessageFound = false; + + observer.OfType() + .Where(x => x.ErrorType == BindingErrorType.DataValidationError) + .Subscribe(_ => validationMessageFound = true); + observer.SetValue(-5); + + Assert.True(validationMessageFound); + } + + [Fact] + public void Indei_Validation_Does_Not_Subscribe_When_DataValidatation_Not_Enabled() + { + var data = new IndeiTest { MustBePositive = 5 }; + var observer = new ExpressionObserver(data, nameof(data.MustBePositive), false); + + observer.Subscribe(_ => { }); + + Assert.Equal(0, data.ErrorsChangedSubscriptionCount); + } + + [Fact] + public void Enabled_Indei_Validation_Subscribes() + { + var data = new IndeiTest { MustBePositive = 5 }; + var observer = new ExpressionObserver(data, nameof(data.MustBePositive), true); + var sub = observer.Subscribe(_ => { }); + + Assert.Equal(1, data.ErrorsChangedSubscriptionCount); + sub.Dispose(); + Assert.Equal(0, data.ErrorsChangedSubscriptionCount); + } + + [Fact] + public void Validation_Plugins_Send_Correct_Notifications() + { + var data = new IndeiTest(); + var observer = new ExpressionObserver(data, nameof(data.MustBePositive), true); + var result = new List(); + + observer.Subscribe(x => result.Add(x)); + observer.SetValue(5); + observer.SetValue(-5); + observer.SetValue("foo"); + observer.SetValue(5); + + Assert.Equal(new[] + { + new BindingNotification(0), + + // Value is notified twice as ErrorsChanged is always called by IndeiTest. + new BindingNotification(5), + new BindingNotification(5), + + // Value is first signalled without an error as validation hasn't been updated. + new BindingNotification(-5), + new BindingNotification(new Exception("Must be positive"), BindingErrorType.DataValidationError, -5), + + // Exception is thrown by trying to set value to "foo". + new BindingNotification( + new ArgumentException("Object of type 'System.String' cannot be converted to type 'System.Int32'."), + BindingErrorType.DataValidationError), + + // Value is set then validation is updated. + new BindingNotification(new Exception("Must be positive"), BindingErrorType.DataValidationError, 5), + new BindingNotification(5), + }, result); + } + + [Fact] + public void Doesnt_Subscribe_To_Indei_Of_Intermediate_Object_In_Chain() + { + var data = new Container + { + Inner = new IndeiTest() + }; + + var observer = new ExpressionObserver( + data, + $"{nameof(Container.Inner)}.{nameof(IndeiTest.MustBePositive)}", + true); + + observer.Subscribe(_ => { }); + + // We may want to change this but I've never seen an example of data validation on an + // intermediate object in a chain so for the moment I'm not sure what the result of + // validating such a thing should look like. + Assert.Equal(0, data.ErrorsChangedSubscriptionCount); + Assert.Equal(1, ((IndeiTest)data.Inner).ErrorsChangedSubscriptionCount); + } + + [Fact] + public void Sends_Correct_Notifications_With_Property_Chain() + { + var container = new Container(); + var inner = new IndeiTest(); + + var observer = new ExpressionObserver( + container, + $"{nameof(Container.Inner)}.{nameof(IndeiTest.MustBePositive)}", + true); + var result = new List(); + + observer.Subscribe(x => result.Add(x)); + + Assert.Equal(new[] + { + new BindingNotification( + new MarkupBindingChainNullException("Inner.MustBePositive", "Inner"), + BindingErrorType.Error, + AvaloniaProperty.UnsetValue), + }, result); + } + + public class ExceptionTest : NotifyingBase + { + private int _mustBePositive; + + public int MustBePositive + { + get { return _mustBePositive; } + set + { + if (value <= 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + _mustBePositive = value; + RaisePropertyChanged(); + } + } + } + + private class IndeiTest : IndeiBase + { + private int _mustBePositive; + private Dictionary> _errors = new Dictionary>(); + + public int MustBePositive + { + get { return _mustBePositive; } + set + { + _mustBePositive = value; + RaisePropertyChanged(); + + if (value >= 0) + { + _errors.Remove(nameof(MustBePositive)); + RaiseErrorsChanged(nameof(MustBePositive)); + } + else + { + _errors[nameof(MustBePositive)] = new[] { "Must be positive" }; + RaiseErrorsChanged(nameof(MustBePositive)); + } + } + } + + public override bool HasErrors => _mustBePositive >= 0; + + public override IEnumerable GetErrors(string propertyName) + { + IList result; + _errors.TryGetValue(propertyName, out result); + return result; + } + } + + private class Container : IndeiBase + { + private object _inner; + + public object Inner + { + get { return _inner; } + set { _inner = value; RaisePropertyChanged(); } + } + + public override bool HasErrors => false; + public override IEnumerable GetErrors(string propertyName) => null; + } + } +} diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs index b79498baae..f6c4540611 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs @@ -189,58 +189,22 @@ namespace Avalonia.Markup.UnitTests.Data var expected = new[] { "bar", "bar2" }; Assert.Equal(expected, result); - Assert.Equal(0, data.Foo.SubscriptionCount); - } - - [Fact] - public void Should_Not_Keep_Source_Alive_ObservableCollection() - { - Func> run = () => - { - var source = new { Foo = new AvaloniaList { "foo", "bar" } }; - var target = new ExpressionObserver(source, "Foo"); - return Tuple.Create(target, new WeakReference(source.Foo)); - }; - - var result = run(); - result.Item1.Subscribe(x => { }); - - GC.Collect(); - - Assert.Null(result.Item2.Target); - } - - [Fact] - public void Should_Not_Keep_Source_Alive_NonIntegerIndexer() - { - Func> run = () => - { - var source = new NonIntegerIndexer(); - var target = new ExpressionObserver(source, "Foo"); - return Tuple.Create(target, new WeakReference(source)); - }; - - var result = run(); - result.Item1.Subscribe(x => { }); - - GC.Collect(); - - Assert.Null(result.Item2.Target); + Assert.Equal(0, data.Foo.PropertyChangedSubscriptionCount); } private class NonIntegerIndexer : NotifyingBase { - private Dictionary storage = new Dictionary(); + private readonly Dictionary _storage = new Dictionary(); public string this[string key] { get { - return storage[key]; + return _storage[key]; } set { - storage[key] = value; + _storage[key] = value; RaisePropertyChanged(CommonPropertyNames.IndexerName); } } diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs index 9fa753917c..2a2bf06bf1 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Lifetime.cs @@ -27,6 +27,19 @@ namespace Avalonia.Markup.UnitTests.Data Assert.True(completed); } + [Fact] + public void Should_Complete_When_Source_Observable_Errors() + { + var source = new BehaviorSubject(1); + var target = new ExpressionObserver(source, "Foo"); + var completed = false; + + target.Subscribe(_ => { }, () => completed = true); + source.OnError(new Exception()); + + Assert.True(completed); + } + [Fact] public void Should_Complete_When_Update_Observable_Completes() { @@ -40,6 +53,19 @@ namespace Avalonia.Markup.UnitTests.Data Assert.True(completed); } + [Fact] + public void Should_Complete_When_Update_Observable_Errors() + { + var update = new Subject(); + var target = new ExpressionObserver(() => 1, "Foo", update); + var completed = false; + + target.Subscribe(_ => { }, () => completed = true); + update.OnError(new Exception()); + + Assert.True(completed); + } + [Fact] public void Should_Unsubscribe_From_Source_Observable() { @@ -55,7 +81,7 @@ namespace Avalonia.Markup.UnitTests.Data scheduler.Start(); } - Assert.Equal(new[] { AvaloniaProperty.UnsetValue, "foo" }, result); + Assert.Equal(new[] { "foo" }, result); Assert.All(source.Subscriptions, x => Assert.NotEqual(Subscription.Infinite, x.Unsubscribe)); } @@ -77,22 +103,6 @@ namespace Avalonia.Markup.UnitTests.Data Assert.All(update.Subscriptions, x => Assert.NotEqual(Subscription.Infinite, x.Unsubscribe)); } - [Fact] - public void Should_Set_Node_Target_To_Null_On_Unsubscribe() - { - var target = new ExpressionObserver(new { Foo = "foo" }, "Foo"); - var result = new List(); - - using (target.Subscribe(x => result.Add(x))) - using (target.Subscribe(_ => { })) - { - Assert.NotNull(target.Node.Target); - } - - Assert.Equal(new[] { "foo" }, result); - Assert.Null(target.Node.Target); - } - private Recorded> OnNext(long time, object value) { return new Recorded>(time, Notification.CreateOnNext(value)); diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Negation.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Negation.cs index b3046118be..6bee0d10f4 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Negation.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Negation.cs @@ -3,6 +3,7 @@ using System; using System.Reactive.Linq; +using Avalonia.Data; using Avalonia.Markup.Data; using Xunit; @@ -61,23 +62,31 @@ namespace Avalonia.Markup.UnitTests.Data } [Fact] - public async void Should_Return_UnsetValue_For_String_Not_Convertible_To_Boolean() + public async void Should_Return_BindingNotification_For_String_Not_Convertible_To_Boolean() { var data = new { Foo = "foo" }; var target = new ExpressionObserver(data, "!Foo"); var result = await target.Take(1); - Assert.Equal(AvaloniaProperty.UnsetValue, result); + Assert.Equal( + new BindingNotification( + new InvalidCastException($"Unable to convert 'foo' to bool."), + BindingErrorType.Error), + result); } [Fact] - public async void Should_Return_Empty_For_Value_Not_Convertible_To_Boolean() + public async void Should_Return_BindingNotification_For_Value_Not_Convertible_To_Boolean() { var data = new { Foo = new object() }; var target = new ExpressionObserver(data, "!Foo"); var result = await target.Take(1); - Assert.Equal(AvaloniaProperty.UnsetValue, result); + Assert.Equal( + new BindingNotification( + new InvalidCastException($"Unable to convert 'System.Object' to bool."), + BindingErrorType.Error), + result); } [Fact] diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs index c5bb2886b5..3263aaace2 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Reactive.Linq; using System.Reactive.Subjects; +using Avalonia.Data; using Avalonia.Markup.Data; using Avalonia.UnitTests; using Xunit; @@ -27,7 +28,7 @@ namespace Avalonia.Markup.UnitTests.Data source.OnNext("bar"); sync.ExecutePostedCallbacks(); - Assert.Equal(new[] { AvaloniaProperty.UnsetValue, "foo", "bar" }, result); + Assert.Equal(new[] { "foo", "bar" }, result); } } @@ -44,10 +45,50 @@ namespace Avalonia.Markup.UnitTests.Data data.Next.OnNext(new Class2("foo")); sync.ExecutePostedCallbacks(); - Assert.Equal(new[] { AvaloniaProperty.UnsetValue, "foo" }, result); + Assert.Equal(new[] { "foo" }, result); sub.Dispose(); - Assert.Equal(0, data.SubscriptionCount); + Assert.Equal(0, data.PropertyChangedSubscriptionCount); + } + } + + [Fact] + public void Should_Get_Simple_Observable_Value_With_DataValidation_Enabled() + { + using (var sync = UnitTestSynchronizationContext.Begin()) + { + var source = new BehaviorSubject("foo"); + var data = new { Foo = source }; + var target = new ExpressionObserver(data, "Foo", true); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x)); + source.OnNext("bar"); + sync.ExecutePostedCallbacks(); + + // What does it mean to have data validation on an observable? Without a use-case + // it's hard to know what to do here so for the moment the value is returned. + Assert.Equal(new[] { "foo", "bar" }, result); + } + } + + [Fact] + public void Should_Get_Property_Value_From_Observable_With_DataValidation_Enabled() + { + using (var sync = UnitTestSynchronizationContext.Begin()) + { + var data = new Class1(); + var target = new ExpressionObserver(data, "Next.Foo", true); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x)); + data.Next.OnNext(new Class2("foo")); + sync.ExecutePostedCallbacks(); + + Assert.Equal(new[] { new BindingNotification("foo") }, result); + + sub.Dispose(); + Assert.Equal(0, data.PropertyChangedSubscriptionCount); } } diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs index 643b8fccab..aa9ee7d58b 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs @@ -32,6 +32,8 @@ namespace Avalonia.Markup.UnitTests.Data var data = new { Foo = "foo" }; var target = new ExpressionObserver(data, "Foo"); + target.Subscribe(_ => { }); + Assert.Equal(typeof(string), target.ResultType); } @@ -55,6 +57,46 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal("foo", result); } + [Fact] + public async void Should_Return_UnsetValue_For_Root_Null() + { + var data = new Class3 { Foo = "foo" }; + var target = new ExpressionObserver(default(object), "Foo"); + var result = await target.Take(1); + + Assert.Equal(AvaloniaProperty.UnsetValue, result); + } + + [Fact] + public async void Should_Return_UnsetValue_For_Root_UnsetValue() + { + var data = new Class3 { Foo = "foo" }; + var target = new ExpressionObserver(AvaloniaProperty.UnsetValue, "Foo"); + var result = await target.Take(1); + + Assert.Equal(AvaloniaProperty.UnsetValue, result); + } + + [Fact] + public async void Should_Return_UnsetValue_For_Observable_Root_Null() + { + var data = new Class3 { Foo = "foo" }; + var target = new ExpressionObserver(Observable.Return(default(object)), "Foo"); + var result = await target.Take(1); + + Assert.Equal(AvaloniaProperty.UnsetValue, result); + } + + [Fact] + public async void Should_Return_UnsetValue_For_Observable_Root_UnsetValue() + { + var data = new Class3 { Foo = "foo" }; + var target = new ExpressionObserver(Observable.Return(AvaloniaProperty.UnsetValue), "Foo"); + var result = await target.Take(1); + + Assert.Equal(AvaloniaProperty.UnsetValue, result); + } + [Fact] public async void Should_Get_Simple_Property_Chain() { @@ -71,21 +113,44 @@ namespace Avalonia.Markup.UnitTests.Data var data = new { Foo = new { Bar = new { Baz = "baz" } } }; var target = new ExpressionObserver(data, "Foo.Bar.Baz"); + target.Subscribe(_ => { }); + Assert.Equal(typeof(string), target.ResultType); } [Fact] - public async void Should_Return_BindingError_For_Broken_Chain() + public async void Should_Return_BindingNotification_Error_For_Broken_Chain() { var data = new { Foo = new { Bar = 1 } }; var target = new ExpressionObserver(data, "Foo.Bar.Baz"); var result = await target.Take(1); - Assert.IsType(result); + Assert.IsType(result); - var error = result as BindingError; - Assert.IsType(error.Exception); - Assert.Equal("Could not find CLR property 'Baz' on '1'", error.Exception.Message); + Assert.Equal( + new BindingNotification( + new MissingMemberException("Could not find CLR property 'Baz' on '1'"), BindingErrorType.Error), + result); + } + + [Fact] + public void Should_Return_BindingNotification_Error_For_Chain_With_Null_Value() + { + var data = new { Foo = default(object) }; + var target = new ExpressionObserver(data, "Foo.Bar.Baz"); + var result = new List(); + + target.Subscribe(x => result.Add(x)); + + Assert.Equal( + new[] + { + new BindingNotification( + new MarkupBindingChainNullException("Foo.Bar.Baz", "Foo"), + BindingErrorType.Error, + AvaloniaProperty.UnsetValue), + }, + result); } [Fact] @@ -111,7 +176,7 @@ namespace Avalonia.Markup.UnitTests.Data sub.Dispose(); - Assert.Equal(0, data.SubscriptionCount); + Assert.Equal(0, data.PropertyChangedSubscriptionCount); } [Fact] @@ -139,7 +204,7 @@ namespace Avalonia.Markup.UnitTests.Data sub.Dispose(); - Assert.Equal(0, data.SubscriptionCount); + Assert.Equal(0, data.PropertyChangedSubscriptionCount); } [Fact] @@ -151,13 +216,14 @@ namespace Avalonia.Markup.UnitTests.Data var sub = target.Subscribe(x => result.Add(x)); ((Class2)data.Next).Bar = "baz"; + ((Class2)data.Next).Bar = null; - Assert.Equal(new[] { "bar", "baz" }, result); + Assert.Equal(new[] { "bar", "baz", null }, result); sub.Dispose(); - Assert.Equal(0, data.SubscriptionCount); - Assert.Equal(0, data.Next.SubscriptionCount); + Assert.Equal(0, data.PropertyChangedSubscriptionCount); + Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount); } [Fact] @@ -170,39 +236,60 @@ namespace Avalonia.Markup.UnitTests.Data var sub = target.Subscribe(x => result.Add(x)); var old = data.Next; data.Next = new Class2 { Bar = "baz" }; + data.Next = new Class2 { Bar = null }; - Assert.Equal(new[] { "bar", "baz" }, result); + Assert.Equal(new[] { "bar", "baz", null }, result); sub.Dispose(); - Assert.Equal(0, data.SubscriptionCount); - Assert.Equal(0, data.Next.SubscriptionCount); - Assert.Equal(0, old.SubscriptionCount); + Assert.Equal(0, data.PropertyChangedSubscriptionCount); + Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount); + Assert.Equal(0, old.PropertyChangedSubscriptionCount); } [Fact] public void Should_Track_Property_Chain_Breaking_With_Null_Then_Mending() { - var data = new Class1 { Next = new Class2 { Bar = "bar" } }; - var target = new ExpressionObserver(data, "Next.Bar"); + var data = new Class1 + { + Next = new Class2 + { + Next = new Class2 + { + Bar = "bar" + } + } + }; + + var target = new ExpressionObserver(data, "Next.Next.Bar"); var result = new List(); var sub = target.Subscribe(x => result.Add(x)); var old = data.Next; - data.Next = null; data.Next = new Class2 { Bar = "baz" }; + data.Next = old; - Assert.Equal(new[] { "bar", AvaloniaProperty.UnsetValue, "baz" }, result); + Assert.Equal( + new object[] + { + "bar", + new BindingNotification( + new MarkupBindingChainNullException("Next.Next.Bar", "Next.Next"), + BindingErrorType.Error, + AvaloniaProperty.UnsetValue), + "bar" + }, + result); sub.Dispose(); - Assert.Equal(0, data.SubscriptionCount); - Assert.Equal(0, data.Next.SubscriptionCount); - Assert.Equal(0, old.SubscriptionCount); + Assert.Equal(0, data.PropertyChangedSubscriptionCount); + Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount); + Assert.Equal(0, old.PropertyChangedSubscriptionCount); } [Fact] - public void Should_Track_Property_Chain_Breaking_With_Object_Then_Mending() + public void Should_Track_Property_Chain_Breaking_With_Missing_Member_Then_Mending() { var data = new Class1 { Next = new Class2 { Bar = "bar" } }; var target = new ExpressionObserver(data, "Next.Bar"); @@ -214,17 +301,23 @@ namespace Avalonia.Markup.UnitTests.Data data.Next = breaking; data.Next = new Class2 { Bar = "baz" }; - Assert.Equal(3, result.Count); - Assert.Equal("bar", result[0]); - Assert.IsType(result[1]); - Assert.Equal("baz", result[2]); + Assert.Equal( + new object[] + { + "bar", + new BindingNotification( + new MissingMemberException("Could not find CLR property 'Bar' on 'Avalonia.Markup.UnitTests.Data.ExpressionObserverTests_Property+WithoutBar'"), + BindingErrorType.Error), + "baz", + }, + result); sub.Dispose(); - Assert.Equal(0, data.SubscriptionCount); - Assert.Equal(0, data.Next.SubscriptionCount); - Assert.Equal(0, breaking.SubscriptionCount); - Assert.Equal(0, old.SubscriptionCount); + Assert.Equal(0, data.PropertyChangedSubscriptionCount); + Assert.Equal(0, data.Next.PropertyChangedSubscriptionCount); + Assert.Equal(0, breaking.PropertyChangedSubscriptionCount); + Assert.Equal(0, old.PropertyChangedSubscriptionCount); } [Fact] @@ -258,17 +351,59 @@ namespace Avalonia.Markup.UnitTests.Data scheduler.Start(); } - Assert.Equal(new[] { AvaloniaProperty.UnsetValue, "foo", "bar" }, result); + Assert.Equal(new[] { "foo", "bar" }, result); Assert.All(source.Subscriptions, x => Assert.NotEqual(Subscription.Infinite, x.Unsubscribe)); } + [Fact] + public void Subscribing_Multiple_Times_Should_Return_Values_To_All() + { + var data = new Class1 { Foo = "foo" }; + var target = new ExpressionObserver(data, "Foo"); + var result1 = new List(); + var result2 = new List(); + var result3 = new List(); + + target.Subscribe(x => result1.Add(x)); + target.Subscribe(x => result2.Add(x)); + + data.Foo = "bar"; + + target.Subscribe(x => result3.Add(x)); + + Assert.Equal(new[] { "foo", "bar" }, result1); + Assert.Equal(new[] { "foo", "bar" }, result2); + Assert.Equal(new[] { "bar" }, result3); + } + + [Fact] + public void Subscribing_Multiple_Times_Should_Only_Add_PropertyChanged_Handlers_Once() + { + var data = new Class1 { Foo = "foo" }; + var target = new ExpressionObserver(data, "Foo"); + + var sub1 = target.Subscribe(x => { }); + var sub2 = target.Subscribe(x => { }); + + Assert.Equal(1, data.PropertyChangedSubscriptionCount); + + sub1.Dispose(); + sub2.Dispose(); + + Assert.Equal(0, data.PropertyChangedSubscriptionCount); + } + [Fact] public void SetValue_Should_Set_Simple_Property_Value() { var data = new Class1 { Foo = "foo" }; var target = new ExpressionObserver(data, "Foo"); - Assert.True(target.SetValue("bar")); + using (target.Subscribe(_ => { })) + { + Assert.True(target.SetValue("bar")); + } + Assert.Equal("bar", data.Foo); } @@ -278,7 +413,11 @@ namespace Avalonia.Markup.UnitTests.Data var data = new Class1 { Next = new Class2 { Bar = "bar" } }; var target = new ExpressionObserver(data, "Next.Bar"); - Assert.True(target.SetValue("baz")); + using (target.Subscribe(_ => { })) + { + Assert.True(target.SetValue("baz")); + } + Assert.Equal("baz", ((Class2)data.Next).Bar); } @@ -288,25 +427,48 @@ namespace Avalonia.Markup.UnitTests.Data var data = new Class1 { Next = new WithoutBar()}; var target = new ExpressionObserver(data, "Next.Bar"); - Assert.False(target.SetValue("baz")); + using (target.Subscribe(_ => { })) + { + Assert.False(target.SetValue("baz")); + } } [Fact] - public void SetValue_Should_Return_False_For_Missing_Object() + public void SetValue_Should_Notify_New_Value_With_Inpc() { var data = new Class1(); - var target = new ExpressionObserver(data, "Next.Bar"); + var target = new ExpressionObserver(data, "Foo"); + var result = new List(); - Assert.False(target.SetValue("baz")); + target.Subscribe(x => result.Add(x)); + target.SetValue("bar"); + + Assert.Equal(new[] { null, "bar" }, result); } [Fact] - public async void Should_Handle_Null_Root() + public void SetValue_Should_Notify_New_Value_Without_Inpc() { - var target = new ExpressionObserver((object)null, "Foo"); - var result = await target.Take(1); + var data = new Class1(); + var target = new ExpressionObserver(data, "Bar"); + var result = new List(); - Assert.Equal(AvaloniaProperty.UnsetValue, result); + target.Subscribe(x => result.Add(x)); + target.SetValue("bar"); + + Assert.Equal(new[] { null, "bar" }, result); + } + + [Fact] + public void SetValue_Should_Return_False_For_Missing_Object() + { + var data = new Class1(); + var target = new ExpressionObserver(data, "Next.Bar"); + + using (target.Subscribe(_ => { })) + { + Assert.False(target.SetValue("baz")); + } } [Fact] @@ -325,10 +487,17 @@ namespace Avalonia.Markup.UnitTests.Data root = null; update.OnNext(Unit.Default); - Assert.Equal(new[] { "foo", "bar", AvaloniaProperty.UnsetValue }, result); - - Assert.Equal(0, first.SubscriptionCount); - Assert.Equal(0, second.SubscriptionCount); + Assert.Equal( + new object[] + { + "foo", + "bar", + AvaloniaProperty.UnsetValue, + }, + result); + + Assert.Equal(0, first.PropertyChangedSubscriptionCount); + Assert.Equal(0, second.PropertyChangedSubscriptionCount); } [Fact] @@ -351,7 +520,7 @@ namespace Avalonia.Markup.UnitTests.Data private interface INext { - int SubscriptionCount { get; } + int PropertyChangedSubscriptionCount { get; } } private class Class1 : NotifyingBase @@ -390,6 +559,7 @@ namespace Avalonia.Markup.UnitTests.Data private class Class2 : NotifyingBase, INext { private string _bar; + private INext _next; public string Bar { @@ -400,6 +570,16 @@ namespace Avalonia.Markup.UnitTests.Data RaisePropertyChanged(nameof(Bar)); } } + + public INext Next + { + get { return _next; } + set + { + _next = value; + RaisePropertyChanged(nameof(Next)); + } + } } private class Class3 : Class1 diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_SetValue.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_SetValue.cs index 4dabd34460..3238435841 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_SetValue.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_SetValue.cs @@ -18,7 +18,10 @@ namespace Avalonia.Markup.UnitTests.Data var data = new { Foo = "foo" }; var target = new ExpressionObserver(data, "Foo"); - target.SetValue("bar"); + using (target.Subscribe(_ => { })) + { + target.SetValue("bar"); + } Assert.Equal("foo", data.Foo); } @@ -29,7 +32,10 @@ namespace Avalonia.Markup.UnitTests.Data var data = new Class1 { Foo = new Class2 { Bar = "bar" } }; var target = new ExpressionObserver(data, "Foo.Bar"); - target.SetValue("foo"); + using (target.Subscribe(_ => { })) + { + target.SetValue("foo"); + } Assert.Equal("foo", data.Foo.Bar); } diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Task.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Task.cs index 3d4c0b1b43..3dcd8a4fbc 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Task.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Task.cs @@ -4,8 +4,8 @@ using System; using System.Collections.Generic; using System.Reactive.Linq; -using System.Threading; using System.Threading.Tasks; +using Avalonia.Data; using Avalonia.Markup.Data; using Avalonia.UnitTests; using Xunit; @@ -28,7 +28,7 @@ namespace Avalonia.Markup.UnitTests.Data tcs.SetResult("foo"); sync.ExecutePostedCallbacks(); - Assert.Equal(new object[] { AvaloniaProperty.UnsetValue, "foo" }, result.ToArray()); + Assert.Equal(new[] { "foo" }, result); } } @@ -43,7 +43,7 @@ namespace Avalonia.Markup.UnitTests.Data var sub = target.Subscribe(x => result.Add(x)); - Assert.Equal(new object[] { "foo" }, result.ToArray()); + Assert.Equal(new[] { "foo" }, result); } } @@ -61,10 +61,84 @@ namespace Avalonia.Markup.UnitTests.Data tcs.SetResult(new Class2("foo")); sync.ExecutePostedCallbacks(); - Assert.Equal(new object[] { AvaloniaProperty.UnsetValue, "foo" }, result.ToArray()); + Assert.Equal(new[] { "foo" }, result); } } + [Fact] + public void Should_Return_BindingNotification_Error_On_Task_Exception() + { + using (var sync = UnitTestSynchronizationContext.Begin()) + { + var tcs = new TaskCompletionSource(); + var data = new { Foo = tcs.Task }; + var target = new ExpressionObserver(data, "Foo"); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x)); + tcs.SetException(new NotSupportedException()); + sync.ExecutePostedCallbacks(); + + Assert.Equal( + new[] + { + new BindingNotification( + new AggregateException(new NotSupportedException()), + BindingErrorType.Error) + }, + result); + } + } + + [Fact] + public void Should_Return_BindingNotification_Error_For_Faulted_Task() + { + using (var sync = UnitTestSynchronizationContext.Begin()) + { + var data = new { Foo = TaskFromException(new NotSupportedException()) }; + var target = new ExpressionObserver(data, "Foo"); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x)); + + Assert.Equal( + new[] + { + new BindingNotification( + new AggregateException(new NotSupportedException()), + BindingErrorType.Error) + }, + result); + } + } + + [Fact] + public void Should_Get_Simple_Task_Value_With_Data_DataValidation_Enabled() + { + using (var sync = UnitTestSynchronizationContext.Begin()) + { + var tcs = new TaskCompletionSource(); + var data = new { Foo = tcs.Task }; + var target = new ExpressionObserver(data, "Foo", true); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x)); + tcs.SetResult("foo"); + sync.ExecutePostedCallbacks(); + + // What does it mean to have data validation on a Task? Without a use-case it's + // hard to know what to do here so for the moment the value is returned. + Assert.Equal(new [] { "foo" }, result); + } + } + + private Task TaskFromException(Exception e) + { + var tcs = new TaskCompletionSource(); + tcs.SetException(e); + return tcs.Task; + } + private class Class1 : NotifyingBase { public Class1(Task next) diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Validation.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Validation.cs deleted file mode 100644 index 59c8965cfb..0000000000 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionObserverTests_Validation.cs +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Collections; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Reactive.Linq; -using System.Runtime.CompilerServices; -using Avalonia.Data; -using Avalonia.Markup.Data; -using Xunit; - -namespace Avalonia.Markup.UnitTests.Data -{ - public class ExpressionObserverTests_Validation - { - [Fact] - public void Exception_Validation_Sends_ValidationUpdate() - { - var data = new ExceptionTest { MustBePositive = 5 }; - var observer = new ExpressionObserver(data, nameof(data.MustBePositive), false); - var validationMessageFound = false; - observer.Where(o => o is IValidationStatus).Subscribe(_ => validationMessageFound = true); - observer.SetValue(-5); - Assert.True(validationMessageFound); - } - - [Fact] - public void Disabled_Indei_Validation_Does_Not_Subscribe() - { - var data = new IndeiTest { MustBePositive = 5 }; - var observer = new ExpressionObserver(data, nameof(data.MustBePositive), false); - - observer.Subscribe(_ => { }); - - Assert.Equal(0, data.SubscriptionCount); - } - - [Fact] - public void Enabled_Indei_Validation_Subscribes() - { - var data = new IndeiTest { MustBePositive = 5 }; - var observer = new ExpressionObserver(data, nameof(data.MustBePositive), true); - var sub = observer.Subscribe(_ => { }); - - Assert.Equal(1, data.SubscriptionCount); - sub.Dispose(); - Assert.Equal(0, data.SubscriptionCount); - } - - public class ExceptionTest : INotifyPropertyChanged - { - private int _mustBePositive; - - public int MustBePositive - { - get { return _mustBePositive; } - set - { - if (value <= 0) - { - throw new ArgumentOutOfRangeException(nameof(value)); - } - _mustBePositive = value; - } - } - - public event PropertyChangedEventHandler PropertyChanged; - - private void NotifyPropertyChanged([CallerMemberName] string propertyName = "") - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - } - - private class IndeiTest : INotifyDataErrorInfo - { - private int _mustBePositive; - private Dictionary> _errors = new Dictionary>(); - private EventHandler _errorsChanged; - - public int MustBePositive - { - get { return _mustBePositive; } - set - { - if (value >= 0) - { - _mustBePositive = value; - _errors.Remove(nameof(MustBePositive)); - _errorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(MustBePositive))); - } - else - { - _errors[nameof(MustBePositive)] = new[] { "Must be positive" }; - _errorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(MustBePositive))); - } - } - } - - public bool HasErrors => _mustBePositive >= 0; - - public int SubscriptionCount { get; private set; } - - public event EventHandler ErrorsChanged - { - add - { - _errorsChanged += value; - ++SubscriptionCount; - } - remove - { - _errorsChanged -= value; - --SubscriptionCount; - } - } - - public IEnumerable GetErrors(string propertyName) - { - IList result; - _errors.TryGetValue(propertyName, out result); - return result; - } - } - } -} diff --git a/tests/Avalonia.Markup.UnitTests/Data/ExpressionSubjectTests.cs b/tests/Avalonia.Markup.UnitTests/Data/ExpressionSubjectTests.cs deleted file mode 100644 index 0b6a507274..0000000000 --- a/tests/Avalonia.Markup.UnitTests/Data/ExpressionSubjectTests.cs +++ /dev/null @@ -1,198 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.ComponentModel; -using System.Globalization; -using System.Reactive.Linq; -using Moq; -using Avalonia.Data; -using Avalonia.Markup.Data; -using Xunit; -using System.Threading; - -namespace Avalonia.Markup.UnitTests.Data -{ - public class ExpressionSubjectTests - { - [Fact] - public async void Should_Get_Simple_Property_Value() - { - var data = new Class1 { StringValue = "foo" }; - var target = new ExpressionSubject(new ExpressionObserver(data, "StringValue"), typeof(string)); - var result = await target.Take(1); - - Assert.Equal("foo", result); - } - - [Fact] - public void Should_Set_Simple_Property_Value() - { - var data = new Class1 { StringValue = "foo" }; - var target = new ExpressionSubject(new ExpressionObserver(data, "StringValue"), typeof(string)); - - target.OnNext("bar"); - - Assert.Equal("bar", data.StringValue); - } - - [Fact] - public async void Should_Convert_Get_String_To_Double() - { - Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; - - var data = new Class1 { StringValue = "5.6" }; - var target = new ExpressionSubject(new ExpressionObserver(data, "StringValue"), typeof(double)); - var result = await target.Take(1); - - Assert.Equal(5.6, result); - } - - [Fact] - public async void Getting_Invalid_Double_String_Should_Return_BindingError() - { - var data = new Class1 { StringValue = "foo" }; - var target = new ExpressionSubject(new ExpressionObserver(data, "StringValue"), typeof(double)); - var result = await target.Take(1); - - Assert.IsType(result); - } - - [Fact] - public async void Should_Coerce_Get_Null_Double_String_To_UnsetValue() - { - var data = new Class1 { StringValue = null }; - var target = new ExpressionSubject(new ExpressionObserver(data, "StringValue"), typeof(double)); - var result = await target.Take(1); - - Assert.Equal(AvaloniaProperty.UnsetValue, result); - } - - [Fact] - public void Should_Convert_Set_String_To_Double() - { - Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; - - var data = new Class1 { StringValue = (5.6).ToString() }; - var target = new ExpressionSubject(new ExpressionObserver(data, "StringValue"), typeof(double)); - - target.OnNext(6.7); - - Assert.Equal((6.7).ToString(), data.StringValue); - } - - [Fact] - public async void Should_Convert_Get_Double_To_String() - { - Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; - - var data = new Class1 { DoubleValue = 5.6 }; - var target = new ExpressionSubject(new ExpressionObserver(data, "DoubleValue"), typeof(string)); - var result = await target.Take(1); - - Assert.Equal((5.6).ToString(), result); - } - - [Fact] - public void Should_Convert_Set_Double_To_String() - { - Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; - - var data = new Class1 { DoubleValue = 5.6 }; - var target = new ExpressionSubject(new ExpressionObserver(data, "DoubleValue"), typeof(string)); - - target.OnNext("6.7"); - - Assert.Equal(6.7, data.DoubleValue); - } - - [Fact] - public void Setting_Invalid_Double_String_Should_Not_Change_Target() - { - var data = new Class1 { DoubleValue = 5.6 }; - var target = new ExpressionSubject(new ExpressionObserver(data, "DoubleValue"), typeof(string)); - - target.OnNext("foo"); - - Assert.Equal(5.6, data.DoubleValue); - } - - [Fact] - public void Setting_Invalid_Double_String_Should_Use_FallbackValue() - { - var data = new Class1 { DoubleValue = 5.6 }; - var target = new ExpressionSubject( - new ExpressionObserver(data, "DoubleValue"), - typeof(string), - "9.8", - DefaultValueConverter.Instance); - - target.OnNext("foo"); - - Assert.Equal(9.8, data.DoubleValue); - } - - [Fact] - public void Should_Coerce_Setting_Null_Double_To_Default_Value() - { - var data = new Class1 { DoubleValue = 5.6 }; - var target = new ExpressionSubject(new ExpressionObserver(data, "DoubleValue"), typeof(string)); - - target.OnNext(null); - - Assert.Equal(0, data.DoubleValue); - } - - [Fact] - public void Should_Coerce_Setting_UnsetValue_Double_To_Default_Value() - { - var data = new Class1 { DoubleValue = 5.6 }; - var target = new ExpressionSubject(new ExpressionObserver(data, "DoubleValue"), typeof(string)); - - target.OnNext(AvaloniaProperty.UnsetValue); - - Assert.Equal(0, data.DoubleValue); - } - - [Fact] - public void Should_Pass_ConverterParameter_To_Convert() - { - var data = new Class1 { DoubleValue = 5.6 }; - var converter = new Mock(); - var target = new ExpressionSubject( - new ExpressionObserver(data, "DoubleValue"), - typeof(string), - converter.Object, - converterParameter: "foo"); - - target.Subscribe(_ => { }); - - converter.Verify(x => x.Convert(5.6, typeof(string), "foo", CultureInfo.CurrentUICulture)); - } - - [Fact] - public void Should_Pass_ConverterParameter_To_ConvertBack() - { - var data = new Class1 { DoubleValue = 5.6 }; - var converter = new Mock(); - var target = new ExpressionSubject( - new ExpressionObserver(data, "DoubleValue"), - typeof(string), - converter.Object, - converterParameter: "foo"); - - target.OnNext("bar"); - - converter.Verify(x => x.ConvertBack("bar", typeof(double), "foo", CultureInfo.CurrentUICulture)); - } - - private class Class1 : INotifyPropertyChanged - { - public event PropertyChangedEventHandler PropertyChanged; - - public string StringValue { get; set; } - - public double DoubleValue { get; set; } - } - } -} diff --git a/tests/Avalonia.Markup.UnitTests/Data/IndeiBase.cs b/tests/Avalonia.Markup.UnitTests/Data/IndeiBase.cs new file mode 100644 index 0000000000..bd0ab71626 --- /dev/null +++ b/tests/Avalonia.Markup.UnitTests/Data/IndeiBase.cs @@ -0,0 +1,32 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using Avalonia.UnitTests; + +namespace Avalonia.Markup.UnitTests.Data +{ + internal abstract class IndeiBase : NotifyingBase, INotifyDataErrorInfo + { + private EventHandler _errorsChanged; + + public abstract bool HasErrors { get; } + public int ErrorsChangedSubscriptionCount { get; private set; } + + public event EventHandler ErrorsChanged + { + add { _errorsChanged += value; ++ErrorsChangedSubscriptionCount; } + remove { _errorsChanged -= value; --ErrorsChangedSubscriptionCount; } + } + + public abstract IEnumerable GetErrors(string propertyName); + + protected void RaiseErrorsChanged([CallerMemberName] string propertyName = "") + { + _errorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName)); + } + } +} diff --git a/tests/Avalonia.Markup.UnitTests/Data/IndeiValidatorTests.cs b/tests/Avalonia.Markup.UnitTests/Data/IndeiValidatorTests.cs deleted file mode 100644 index 20bf164360..0000000000 --- a/tests/Avalonia.Markup.UnitTests/Data/IndeiValidatorTests.cs +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Collections; -using System.ComponentModel; -using System.Runtime.CompilerServices; -using Avalonia.Data; -using Avalonia.Markup.Data.Plugins; -using Xunit; - -namespace Avalonia.Markup.UnitTests.Data -{ - public class IndeiValidatorTests - { - public class Data : INotifyPropertyChanged, INotifyDataErrorInfo - { - private int nonValidated; - - public int NonValidated - { - get { return nonValidated; } - set { nonValidated = value; NotifyPropertyChanged(); } - } - - private int mustBePositive; - - public int MustBePositive - { - get { return mustBePositive; } - set - { - mustBePositive = value; - NotifyErrorsChanged(); - } - } - - public bool HasErrors - { - get - { - return MustBePositive > 0; - } - } - - public event PropertyChangedEventHandler PropertyChanged; - public event EventHandler ErrorsChanged; - - private void NotifyPropertyChanged([CallerMemberName] string propertyName = "") - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - - private void NotifyErrorsChanged([CallerMemberName] string propertyName = "") - { - ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName)); - } - - public IEnumerable GetErrors(string propertyName) - { - if (propertyName == nameof(MustBePositive) && MustBePositive <= 0) - { - yield return $"{nameof(MustBePositive)} must be positive"; - } - } - } - - [Fact] - public void Setting_Non_Validating_Does_Not_Trigger_Validation() - { - var inpcAccessorPlugin = new InpcPropertyAccessorPlugin(); - var validatorPlugin = new IndeiValidationPlugin(); - var data = new Data(); - var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.NonValidated), _ => { }); - IValidationStatus status = null; - var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.NonValidated), accessor, s => status = s); - - validator.SetValue(5, BindingPriority.LocalValue); - - Assert.Null(status); - } - - [Fact] - public void Setting_Validating_Property_To_Valid_Value_Returns_Successful_ValidationStatus() - { - var inpcAccessorPlugin = new InpcPropertyAccessorPlugin(); - var validatorPlugin = new IndeiValidationPlugin(); - var data = new Data(); - var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), _ => { }); - IValidationStatus status = null; - var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor, s => status = s); - - validator.SetValue(5, BindingPriority.LocalValue); - - Assert.True(status.IsValid); - } - - [Fact] - public void Setting_Validating_Property_To_Invalid_Value_Returns_Failed_ValidationStatus() - { - var inpcAccessorPlugin = new InpcPropertyAccessorPlugin(); - var validatorPlugin = new IndeiValidationPlugin(); - var data = new Data(); - var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), _ => { }); - IValidationStatus status = null; - var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor, s => status = s); - - validator.SetValue(-5, BindingPriority.LocalValue); - - Assert.False(status.IsValid); - } - } -} diff --git a/tests/Avalonia.Markup.UnitTests/Data/Plugins/DataAnnotationsValidationPluginTests.cs b/tests/Avalonia.Markup.UnitTests/Data/Plugins/DataAnnotationsValidationPluginTests.cs new file mode 100644 index 0000000000..b873971e7f --- /dev/null +++ b/tests/Avalonia.Markup.UnitTests/Data/Plugins/DataAnnotationsValidationPluginTests.cs @@ -0,0 +1,111 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Avalonia.Data; +using Avalonia.Markup.Data.Plugins; +using Xunit; + +namespace Avalonia.Markup.UnitTests.Data.Plugins +{ + public class DataAnnotationsValidationPluginTests + { + [Fact] + public void Should_Match_Property_With_ValidatorAttribute() + { + var target = new DataAnnotationsValidationPlugin(); + var data = new Data(); + + Assert.True(target.Match(new WeakReference(data), nameof(Data.Between5And10))); + } + + [Fact] + public void Should_Match_Property_With_Multiple_ValidatorAttributes() + { + var target = new DataAnnotationsValidationPlugin(); + var data = new Data(); + + Assert.True(target.Match(new WeakReference(data), nameof(Data.PhoneNumber))); + } + + [Fact] + public void Should_Not_Match_Property_Without_ValidatorAttribute() + { + var target = new DataAnnotationsValidationPlugin(); + var data = new Data(); + + Assert.False(target.Match(new WeakReference(data), nameof(Data.Unvalidated))); + } + + [Fact] + public void Produces_Range_BindingNotificationsx() + { + var inpcAccessorPlugin = new InpcPropertyAccessorPlugin(); + var validatorPlugin = new DataAnnotationsValidationPlugin(); + var data = new Data(); + var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.Between5And10)); + var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Between5And10), accessor); + var result = new List(); + + validator.Subscribe(x => result.Add(x)); + validator.SetValue(3, BindingPriority.LocalValue); + validator.SetValue(7, BindingPriority.LocalValue); + validator.SetValue(11, BindingPriority.LocalValue); + + Assert.Equal(new[] + { + new BindingNotification(5), + new BindingNotification( + new ValidationException("The field Between5And10 must be between 5 and 10."), + BindingErrorType.DataValidationError, + 3), + new BindingNotification(7), + new BindingNotification( + new ValidationException("The field Between5And10 must be between 5 and 10."), + BindingErrorType.DataValidationError, + 11), + }, result); + } + + [Fact] + public void Produces_Aggregate_BindingNotificationsx() + { + var inpcAccessorPlugin = new InpcPropertyAccessorPlugin(); + var validatorPlugin = new DataAnnotationsValidationPlugin(); + var data = new Data(); + var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.PhoneNumber)); + var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.PhoneNumber), accessor); + var result = new List(); + + validator.Subscribe(x => result.Add(x)); + validator.SetValue("123456", BindingPriority.LocalValue); + validator.SetValue("abcdefghijklm", BindingPriority.LocalValue); + + Assert.Equal(new[] + { + new BindingNotification(null), + new BindingNotification("123456"), + new BindingNotification( + new AggregateException( + new ValidationException("The PhoneNumber field is not a valid phone number."), + new ValidationException("The field PhoneNumber must be a string or array type with a maximum length of '10'.")), + BindingErrorType.DataValidationError, + "abcdefghijklm"), + }, result); + } + + private class Data + { + [Range(5, 10)] + public int Between5And10 { get; set; } = 5; + + public int Unvalidated { get; set; } + + [Phone] + [MaxLength(10)] + public string PhoneNumber { get; set; } + } + } +} diff --git a/tests/Avalonia.Markup.UnitTests/Data/Plugins/ExceptionValidationPluginTests.cs b/tests/Avalonia.Markup.UnitTests/Data/Plugins/ExceptionValidationPluginTests.cs new file mode 100644 index 0000000000..4a34791008 --- /dev/null +++ b/tests/Avalonia.Markup.UnitTests/Data/Plugins/ExceptionValidationPluginTests.cs @@ -0,0 +1,63 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Reactive.Linq; +using Avalonia.Data; +using Avalonia.Markup.Data.Plugins; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Markup.UnitTests.Data.Plugins +{ + public class ExceptionValidationPluginTests + { + [Fact] + public void Produces_BindingNotifications() + { + var inpcAccessorPlugin = new InpcPropertyAccessorPlugin(); + var validatorPlugin = new ExceptionValidationPlugin(); + var data = new Data(); + var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive)); + var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor); + var result = new List(); + + validator.Subscribe(x => result.Add(x)); + validator.SetValue(5, BindingPriority.LocalValue); + validator.SetValue(-2, BindingPriority.LocalValue); + validator.SetValue(6, BindingPriority.LocalValue); + + Assert.Equal(new[] + { + new BindingNotification(0), + new BindingNotification(5), + new BindingNotification(new ArgumentOutOfRangeException("value"), BindingErrorType.DataValidationError), + new BindingNotification(6), + }, result); + } + + public class Data : NotifyingBase + { + private int _mustBePositive; + + public int MustBePositive + { + get { return _mustBePositive; } + set + { + if (value <= 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + if (value != _mustBePositive) + { + _mustBePositive = value; + RaisePropertyChanged(); + } + } + } + } + } +} diff --git a/tests/Avalonia.Markup.UnitTests/Data/Plugins/IndeiValidationPluginTests.cs b/tests/Avalonia.Markup.UnitTests/Data/Plugins/IndeiValidationPluginTests.cs new file mode 100644 index 0000000000..788bc25a34 --- /dev/null +++ b/tests/Avalonia.Markup.UnitTests/Data/Plugins/IndeiValidationPluginTests.cs @@ -0,0 +1,127 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reactive.Linq; +using Avalonia.Data; +using Avalonia.Markup.Data.Plugins; +using Xunit; + +namespace Avalonia.Markup.UnitTests.Data.Plugins +{ + public class IndeiValidationPluginTests + { + [Fact] + public void Produces_BindingNotifications() + { + var inpcAccessorPlugin = new InpcPropertyAccessorPlugin(); + var validatorPlugin = new IndeiValidationPlugin(); + var data = new Data { Maximum = 5 }; + var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.Value)); + var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Value), accessor); + var result = new List(); + + validator.Subscribe(x => result.Add(x)); + validator.SetValue(5, BindingPriority.LocalValue); + validator.SetValue(6, BindingPriority.LocalValue); + data.Maximum = 10; + data.Maximum = 5; + + Assert.Equal(new[] + { + new BindingNotification(0), + new BindingNotification(5), + + // Value is first signalled without an error as validation hasn't been updated. + new BindingNotification(6), + + // Then the ErrorsChanged event is fired. + new BindingNotification(new Exception("Must be less than Maximum"), BindingErrorType.DataValidationError, 6), + + // Maximum is changed to 10 so value is now valid. + new BindingNotification(6), + + // And Maximum is changed back to 5. + new BindingNotification(new Exception("Must be less than Maximum"), BindingErrorType.DataValidationError, 6), + }, result); + } + + [Fact] + public void Subscribes_And_Unsubscribes() + { + var inpcAccessorPlugin = new InpcPropertyAccessorPlugin(); + var validatorPlugin = new IndeiValidationPlugin(); + var data = new Data { Maximum = 5 }; + var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.Value)); + var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Value), accessor); + + Assert.Equal(0, data.ErrorsChangedSubscriptionCount); + var sub = validator.Subscribe(_ => { }); + Assert.Equal(1, data.ErrorsChangedSubscriptionCount); + sub.Dispose(); + Assert.Equal(0, data.ErrorsChangedSubscriptionCount); + } + + internal class Data : IndeiBase + { + private int _value; + private int _maximum; + private string _error; + + public override bool HasErrors => _error != null; + + public int Value + { + get { return _value; } + set + { + _value = value; + RaisePropertyChanged(); + UpdateError(); + } + } + + public int Maximum + { + get { return _maximum; } + set + { + _maximum = value; + UpdateError(); + } + } + + public override IEnumerable GetErrors(string propertyName) + { + if (propertyName == nameof(Value) && _error != null) + { + return new[] { _error }; + } + + return null; + } + + private void UpdateError() + { + if (_value <= _maximum) + { + if (_error != null) + { + _error = null; + RaiseErrorsChanged(nameof(Value)); + } + } + else + { + if (_error == null) + { + _error = "Must be less than Maximum"; + RaiseErrorsChanged(nameof(Value)); + } + } + } + } + } +} diff --git a/tests/Avalonia.Markup.UnitTests/DefaultValueConverterTests.cs b/tests/Avalonia.Markup.UnitTests/DefaultValueConverterTests.cs index 2e52dff087..fd28f2e900 100644 --- a/tests/Avalonia.Markup.UnitTests/DefaultValueConverterTests.cs +++ b/tests/Avalonia.Markup.UnitTests/DefaultValueConverterTests.cs @@ -115,7 +115,7 @@ namespace Avalonia.Markup.UnitTests null, CultureInfo.InvariantCulture); - Assert.IsType(result); + Assert.IsType(result); } private enum TestEnum diff --git a/tests/Avalonia.Markup.UnitTests/app.config b/tests/Avalonia.Markup.UnitTests/app.config index fa66e8c206..654f911514 100644 --- a/tests/Avalonia.Markup.UnitTests/app.config +++ b/tests/Avalonia.Markup.UnitTests/app.config @@ -1,11 +1,11 @@ - + - - + + - \ No newline at end of file + diff --git a/tests/Avalonia.Markup.UnitTests/packages.config b/tests/Avalonia.Markup.UnitTests/packages.config index 34563ef392..d264c076fd 100644 --- a/tests/Avalonia.Markup.UnitTests/packages.config +++ b/tests/Avalonia.Markup.UnitTests/packages.config @@ -1,11 +1,14 @@  - + - - - - + + + + + + + diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj index 0e2e11c300..54e61fc7d2 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj @@ -93,9 +93,9 @@ + - diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs index 7d8528c5d7..210ad2ab0b 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs @@ -3,6 +3,8 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; using Avalonia.Controls; using Avalonia.Data; using Avalonia.Markup.Data; @@ -164,17 +166,23 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data [Fact] public void DataContext_Binding_Should_Produce_Correct_Results() { + var viewModel = new { Foo = "bar" }; var root = new Decorator { - DataContext = new { Foo = "bar" }, + DataContext = viewModel, }; var child = new Control(); - var dataContextBinding = new Binding("Foo"); var values = new List(); - child.GetObservable(Border.DataContextProperty).Subscribe(x => values.Add(x)); - child.Bind(ContentControl.DataContextProperty, dataContextBinding); + child.GetObservable(Control.DataContextProperty).Subscribe(x => values.Add(x)); + child.Bind(Control.DataContextProperty, new Binding("Foo")); + + // When binding to DataContext and the target isn't found, the binding should produce + // null rather than UnsetValue in order to not propagate incorrect DataContexts from + // parent controls while things are being set up. This logic is implemented in + // `Avalonia.Markup.Xaml.Binding.Initiate`. + Assert.True(child.IsSet(Control.DataContextProperty)); root.Child = child; @@ -192,7 +200,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data var result = binding.Initiate(target, TextBox.TextProperty).Subject; - Assert.IsType(((ExpressionSubject)result).Converter); + Assert.IsType(((BindingExpression)result).Converter); } [Fact] @@ -208,7 +216,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data var result = binding.Initiate(target, TextBox.TextProperty).Subject; - Assert.Same(converter.Object, ((ExpressionSubject)result).Converter); + Assert.Same(converter.Object, ((BindingExpression)result).Converter); } [Fact] @@ -225,7 +233,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data var result = binding.Initiate(target, TextBox.TextProperty).Subject; - Assert.Same("foo", ((ExpressionSubject)result).ConverterParameter); + Assert.Same("foo", ((BindingExpression)result).ConverterParameter); } [Fact] @@ -267,7 +275,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data /// /// /// - Items is bound to DataContext first, followed by say SelectedIndex - /// - When the ListBox is removed from the visual tree, DataContext becomes null (as it's + /// - When the ListBox is removed from the logical tree, DataContext becomes null (as it's /// inherited) /// - This changes Items to null, which changes SelectedIndex to null as there are no /// longer any items @@ -294,12 +302,12 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data // Bind Foo and Bar to the VM. target.Bind(OldDataContextTest.FooProperty, fooBinding); - target.Bind(OldDataContextTest.BarProperty, barBinding); + //target.Bind(OldDataContextTest.BarProperty, barBinding); target.DataContext = vm; // Make sure the control's Foo and Bar properties are read from the VM Assert.Equal(1, target.GetValue(OldDataContextTest.FooProperty)); - Assert.Equal(2, target.GetValue(OldDataContextTest.BarProperty)); + //Assert.Equal(2, target.GetValue(OldDataContextTest.BarProperty)); // Set DataContext to null. target.DataContext = null; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_DataValidation.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_DataValidation.cs new file mode 100644 index 0000000000..5dd8d0cdf9 --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_DataValidation.cs @@ -0,0 +1,75 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Reactive.Linq; +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.Markup.Data; +using Avalonia.Markup.Xaml.Data; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Data +{ + public class BindingTests_DataValidation + { + [Fact] + public void Initiate_Should_Not_Enable_Data_Validation_With_BindingPriority_LocalValue() + { + var textBlock = new TextBlock + { + DataContext = new Class1(), + }; + + var target = new Binding(nameof(Class1.Foo)); + var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: false); + var subject = (BindingExpression)instanced.Subject; + object result = null; + + subject.Subscribe(x => result = x); + + Assert.IsType(result); + } + + [Fact] + public void Initiate_Should_Enable_Data_Validation_With_BindingPriority_LocalValue() + { + var textBlock = new TextBlock + { + DataContext = new Class1(), + }; + + var target = new Binding(nameof(Class1.Foo)); + var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: true); + var subject = (BindingExpression)instanced.Subject; + object result = null; + + subject.Subscribe(x => result = x); + + Assert.Equal(new BindingNotification("foo"), result); + } + + [Fact] + public void Initiate_Should_Not_Enable_Data_Validation_With_BindingPriority_TemplatedParent() + { + var textBlock = new TextBlock + { + DataContext = new Class1(), + }; + + var target = new Binding(nameof(Class1.Foo)) { Priority = BindingPriority.TemplatedParent }; + var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: true); + var subject = (BindingExpression)instanced.Subject; + object result = null; + + subject.Subscribe(x => result = x); + + Assert.IsType(result); + } + + private class Class1 + { + public string Foo { get; set; } = "foo"; + } + } +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Validation.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Validation.cs index 0fed786f07..8759cb42c5 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Validation.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Validation.cs @@ -1,7 +1,11 @@ using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; using Avalonia.Controls; using Avalonia.Data; using Avalonia.Markup.Xaml.Data; +using Avalonia.UnitTests; using Xunit; namespace Avalonia.Markup.Xaml.UnitTests.Data @@ -9,143 +13,112 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data public class BindingTests_Validation { [Fact] - public void Disabled_Validation_Should_Trigger_Validation_Change_On_Exception() + public void Non_Validated_Property_Does_Not_Receive_BindingNotifications() { var source = new ValidationTestModel { MustBePositive = 5 }; - var target = new TestControl { DataContext = source }; - var binding = new Binding + var target = new TestControl { - Path = nameof(source.MustBePositive), - Mode = BindingMode.TwoWay, - - // Even though EnableValidation = false, exception validation is enabled. - EnableValidation = false, + DataContext = source, + [!TestControl.NonValidatedProperty] = new Binding(nameof(source.MustBePositive)), }; - target.Bind(TestControl.ValidationTestProperty, binding); - - target.ValidationTest = -5; - - Assert.False(target.ValidationStatus.IsValid); + Assert.Empty(target.Notifications); } [Fact] - public void Enabled_Validation_Should_Trigger_Validation_Change_On_Exception() + public void Validated_Direct_Property_Receives_BindingNotifications() { var source = new ValidationTestModel { MustBePositive = 5 }; - var target = new TestControl { DataContext = source }; - var binding = new Binding + var target = new TestControl { - Path = nameof(source.MustBePositive), - Mode = BindingMode.TwoWay, - EnableValidation = true, + DataContext = source, }; - target.Bind(TestControl.ValidationTestProperty, binding); - - target.ValidationTest = -5; - Assert.False(target.ValidationStatus.IsValid); - } - + target.Bind( + TestControl.ValidatedDirectProperty, + new Binding(nameof(source.MustBePositive), BindingMode.TwoWay)); - [Fact] - public void Passed_Validation_Should_Not_Add_Invalid_Pseudo_Class() - { - var control = new TestControl(); - var model = new ValidationTestModel { MustBePositive = 1 }; - var binding = new Binding - { - Path = nameof(model.MustBePositive), - Mode = BindingMode.TwoWay, - EnableValidation = true, - }; + target.ValidatedDirect = 6; + target.ValidatedDirect = -1; + target.ValidatedDirect = 7; - control.Bind(TestControl.ValidationTestProperty, binding); - control.DataContext = model; - Assert.DoesNotContain(control.Classes, x => x == ":invalid"); + Assert.Equal( + new[] + { + new BindingNotification(5), + new BindingNotification(6), + new BindingNotification(new ArgumentOutOfRangeException("value"), BindingErrorType.DataValidationError), + new BindingNotification(7), + }, + target.Notifications.AsEnumerable()); } - [Fact] - public void Failed_Validation_Should_Add_Invalid_Pseudo_Class() + private class TestControl : Control { - var control = new TestControl(); - var model = new ValidationTestModel { MustBePositive = 1 }; - var binding = new Binding + public static readonly StyledProperty NonValidatedProperty = + AvaloniaProperty.Register( + nameof(Validated), + enableDataValidation: false); + + public static readonly StyledProperty ValidatedProperty = + AvaloniaProperty.Register( + nameof(Validated), + enableDataValidation: true); + + public static readonly DirectProperty ValidatedDirectProperty = + AvaloniaProperty.RegisterDirect( + nameof(Validated), + o => o.ValidatedDirect, + (o, v) => o.ValidatedDirect = v, + enableDataValidation: true); + + private int _direct; + + public int NonValidated { - Path = nameof(model.MustBePositive), - Mode = BindingMode.TwoWay, - EnableValidation = true, - }; - - control.Bind(TestControl.ValidationTestProperty, binding); - control.DataContext = model; - control.ValidationTest = -5; - Assert.Contains(control.Classes, x => x == ":invalid"); - } - - [Fact] - public void Failed_Then_Passed_Validation_Should_Remove_Invalid_Pseudo_Class() - { - var control = new TestControl(); - var model = new ValidationTestModel { MustBePositive = 1 }; + get { return GetValue(NonValidatedProperty); } + set { SetValue(NonValidatedProperty, value); } + } - var binding = new Binding + public int Validated { - Path = nameof(model.MustBePositive), - Mode = BindingMode.TwoWay, - EnableValidation = true, - }; - - control.Bind(TestControl.ValidationTestProperty, binding); - control.DataContext = model; - - - control.ValidationTest = -5; - Assert.Contains(control.Classes, x => x == ":invalid"); - control.ValidationTest = 5; - Assert.DoesNotContain(control.Classes, x => x == ":invalid"); - } - - private class TestControl : Control - { - public static readonly StyledProperty ValidationTestProperty - = AvaloniaProperty.Register(nameof(ValidationTest), 1, defaultBindingMode: BindingMode.TwoWay); + get { return GetValue(ValidatedProperty); } + set { SetValue(ValidatedProperty, value); } + } - public int ValidationTest + public int ValidatedDirect { - get - { - return GetValue(ValidationTestProperty); - } - set - { - SetValue(ValidationTestProperty, value); - } + get { return _direct; } + set { SetAndRaise(ValidatedDirectProperty, ref _direct, value); } } - protected override void DataValidationChanged(AvaloniaProperty property, IValidationStatus status) + public IList Notifications { get; } = new List(); + + protected override void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification) { - if (property == ValidationTestProperty) - { - UpdateValidationState(status); - } + Notifications.Add(notification); } } - private class ValidationTestModel + private class ValidationTestModel : NotifyingBase { - private int mustBePositive; + private int _mustBePositive; public int MustBePositive { - get { return mustBePositive; } + get { return _mustBePositive; } set { if (value <= 0) { throw new ArgumentOutOfRangeException(nameof(value)); } - mustBePositive = value; + + if (_mustBePositive != value) + { + _mustBePositive = value; + RaisePropertyChanged(); + } } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs index df62a1ed41..0c2151850f 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs @@ -39,7 +39,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml { if (level == LogEventLevel.Error && area == LogArea.Binding && - mt == "Error binding to {Target}.{Property}: {Message}" && + mt == "Error in binding to {Target}.{Property}: {Message}" && pv.Length == 3 && pv[0] is ProgressBar && object.ReferenceEquals(pv[1], ProgressBar.ValueProperty) && diff --git a/tests/Avalonia.RenderTests/Shapes/PathTests.cs b/tests/Avalonia.RenderTests/Shapes/PathTests.cs index 9250b6a8a0..2e5d528bce 100644 --- a/tests/Avalonia.RenderTests/Shapes/PathTests.cs +++ b/tests/Avalonia.RenderTests/Shapes/PathTests.cs @@ -24,6 +24,260 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes { } +#if AVALONIA_CAIRO + [Fact(Skip = "Broken in Cairo: waiting for Skia")] +#else + [Fact] +#endif + public void Line_Absolute() + { + Decorator target = new Decorator + { + Width = 200, + Height = 200, + Child = new Path + { + Stroke = Brushes.Red, + StrokeThickness = 1, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Data = StreamGeometry.Parse("M 10,190 L 190,10 M0,0M200,200"), + } + }; + + RenderToFile(target); + CompareImages(); + } + +#if AVALONIA_CAIRO + [Fact(Skip = "Broken in Cairo: waiting for Skia")] +#else + [Fact] +#endif + public void Line_Relative() + { + Decorator target = new Decorator + { + Width = 200, + Height = 200, + Child = new Path + { + Stroke = Brushes.Red, + StrokeThickness = 1, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Data = StreamGeometry.Parse("M10,190 l190,-190 M0,0M200,200"), + } + }; + + RenderToFile(target); + CompareImages(); + } + +#if AVALONIA_CAIRO + [Fact(Skip = "Broken in Cairo: waiting for Skia")] +#else + [Fact] +#endif + public void HorizontalLine_Absolute() + { + Decorator target = new Decorator + { + Width = 200, + Height = 200, + Child = new Path + { + Stroke = Brushes.Red, + StrokeThickness = 1, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Data = StreamGeometry.Parse("M190,100 H10 M0,0M200,200"), + } + }; + + RenderToFile(target); + CompareImages(); + } + +#if AVALONIA_CAIRO + [Fact(Skip = "Broken in Cairo: waiting for Skia")] +#else + [Fact] +#endif + public void HorizontalLine_Relative() + { + Decorator target = new Decorator + { + Width = 200, + Height = 200, + Child = new Path + { + Stroke = Brushes.Red, + StrokeThickness = 1, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Data = StreamGeometry.Parse("M190,100 h-180 M0,0M200,200"), + } + }; + + RenderToFile(target); + CompareImages(); + } + +#if AVALONIA_CAIRO + [Fact(Skip = "Broken in Cairo: waiting for Skia")] +#else + [Fact] +#endif + public void VerticalLine_Absolute() + { + Decorator target = new Decorator + { + Width = 200, + Height = 200, + Child = new Path + { + Stroke = Brushes.Red, + StrokeThickness = 1, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Data = StreamGeometry.Parse("M100,190 V10 M0,0M200,200"), + } + }; + + RenderToFile(target); + CompareImages(); + } + +#if AVALONIA_CAIRO + [Fact(Skip = "Broken in Cairo: waiting for Skia")] +#else + [Fact] +#endif + public void VerticalLine_Relative() + { + Decorator target = new Decorator + { + Width = 200, + Height = 200, + Child = new Path + { + Stroke = Brushes.Red, + StrokeThickness = 1, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Data = StreamGeometry.Parse("M100,190 V-180 M0,0M200,200"), + } + }; + + RenderToFile(target); + CompareImages(); + } + +#if AVALONIA_CAIRO + [Fact(Skip = "Broken in Cairo: waiting for Skia")] +#else + [Fact] +#endif + public void CubicBezier_Absolute() + { + Decorator target = new Decorator + { + Width = 200, + Height = 200, + Child = new Path + { + Fill = Brushes.Gray, + Stroke = Brushes.Red, + StrokeThickness = 1, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Data = StreamGeometry.Parse("M190,0 C10,10 190,190 10,190 M0,0M200,200"), + } + }; + + RenderToFile(target); + CompareImages(); + } + +#if AVALONIA_CAIRO + [Fact(Skip = "Broken in Cairo: waiting for Skia")] +#else + [Fact] +#endif + public void CubicBezier_Relative() + { + Decorator target = new Decorator + { + Width = 200, + Height = 200, + Child = new Path + { + Fill = Brushes.Gray, + Stroke = Brushes.Red, + StrokeThickness = 1, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Data = StreamGeometry.Parse("M190,0 c-180,10 0,190 -180,190 M0,0M200,200"), + } + }; + + RenderToFile(target); + CompareImages(); + } + +#if AVALONIA_CAIRO + [Fact(Skip = "Broken in Cairo: waiting for Skia")] +#else + [Fact] +#endif + public void Arc_Absolute() + { + Decorator target = new Decorator + { + Width = 200, + Height = 200, + Child = new Path + { + Fill = Brushes.Gray, + Stroke = Brushes.Red, + StrokeThickness = 1, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Data = StreamGeometry.Parse("M190,100 A90,90 0 1,0 10,100 M0,0M200,200"), + } + }; + + RenderToFile(target); + CompareImages(); + } + +#if AVALONIA_CAIRO + [Fact(Skip = "Broken in Cairo: waiting for Skia")] +#else + [Fact] +#endif + public void Arc_Relative() + { + Decorator target = new Decorator + { + Width = 200, + Height = 200, + Child = new Path + { + Fill = Brushes.Gray, + Stroke = Brushes.Red, + StrokeThickness = 1, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + Data = StreamGeometry.Parse("M190,100 a90,90 0 1,0 -180,0 M0,0M200,200"), + } + }; + + RenderToFile(target); + CompareImages(); + } + [Fact] public void Path_100px_Triangle_Centered() { diff --git a/tests/Avalonia.Styling.UnitTests/SetterTests.cs b/tests/Avalonia.Styling.UnitTests/SetterTests.cs index 4de90b7790..84536fa47b 100644 --- a/tests/Avalonia.Styling.UnitTests/SetterTests.cs +++ b/tests/Avalonia.Styling.UnitTests/SetterTests.cs @@ -27,7 +27,7 @@ namespace Avalonia.Styling.UnitTests var control = new TextBlock(); var subject = new BehaviorSubject("foo"); var descriptor = new InstancedBinding(subject); - var binding = Mock.Of(x => x.Initiate(control, TextBlock.TextProperty, null) == descriptor); + var binding = Mock.Of(x => x.Initiate(control, TextBlock.TextProperty, null, false) == descriptor); var style = Mock.Of(); var setter = new Setter(TextBlock.TextProperty, binding); diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index 5111680873..8f75ec8efc 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs @@ -20,7 +20,7 @@ namespace Avalonia.UnitTests return _windowImpl?.Invoke() ?? Mock.Of(x => x.Scaling == 1); } - public IWindowImpl CreateEmbeddableWindow() + public IEmbeddableWindowImpl CreateEmbeddableWindow() { throw new NotImplementedException(); } diff --git a/tests/Avalonia.UnitTests/NotifyingBase.cs b/tests/Avalonia.UnitTests/NotifyingBase.cs index c1b7a24303..c91e55d34f 100644 --- a/tests/Avalonia.UnitTests/NotifyingBase.cs +++ b/tests/Avalonia.UnitTests/NotifyingBase.cs @@ -16,7 +16,7 @@ namespace Avalonia.UnitTests add { _propertyChanged += value; - ++SubscriptionCount; + ++PropertyChangedSubscriptionCount; } remove @@ -24,12 +24,12 @@ namespace Avalonia.UnitTests if (_propertyChanged?.GetInvocationList().Contains(value) == true) { _propertyChanged -= value; - --SubscriptionCount; + --PropertyChangedSubscriptionCount; } } } - public int SubscriptionCount + public int PropertyChangedSubscriptionCount { get; private set; diff --git a/tests/TestFiles/Cairo/Shapes/Path/Arc_Absolute.expected.png b/tests/TestFiles/Cairo/Shapes/Path/Arc_Absolute.expected.png new file mode 100644 index 0000000000..113cc908e6 Binary files /dev/null and b/tests/TestFiles/Cairo/Shapes/Path/Arc_Absolute.expected.png differ diff --git a/tests/TestFiles/Cairo/Shapes/Path/Arc_Relative.expected.png b/tests/TestFiles/Cairo/Shapes/Path/Arc_Relative.expected.png new file mode 100644 index 0000000000..113cc908e6 Binary files /dev/null and b/tests/TestFiles/Cairo/Shapes/Path/Arc_Relative.expected.png differ diff --git a/tests/TestFiles/Cairo/Shapes/Path/CubicBezier_Absolute.expected.png b/tests/TestFiles/Cairo/Shapes/Path/CubicBezier_Absolute.expected.png new file mode 100644 index 0000000000..8c8f5350ee Binary files /dev/null and b/tests/TestFiles/Cairo/Shapes/Path/CubicBezier_Absolute.expected.png differ diff --git a/tests/TestFiles/Cairo/Shapes/Path/CubicBezier_Relative.expected.png b/tests/TestFiles/Cairo/Shapes/Path/CubicBezier_Relative.expected.png new file mode 100644 index 0000000000..8c8f5350ee Binary files /dev/null and b/tests/TestFiles/Cairo/Shapes/Path/CubicBezier_Relative.expected.png differ diff --git a/tests/TestFiles/Cairo/Shapes/Path/HorizontalLine_Absolute.expected.png b/tests/TestFiles/Cairo/Shapes/Path/HorizontalLine_Absolute.expected.png new file mode 100644 index 0000000000..546ffec3bb Binary files /dev/null and b/tests/TestFiles/Cairo/Shapes/Path/HorizontalLine_Absolute.expected.png differ diff --git a/tests/TestFiles/Cairo/Shapes/Path/HorizontalLine_Relative.expected.png b/tests/TestFiles/Cairo/Shapes/Path/HorizontalLine_Relative.expected.png new file mode 100644 index 0000000000..546ffec3bb Binary files /dev/null and b/tests/TestFiles/Cairo/Shapes/Path/HorizontalLine_Relative.expected.png differ diff --git a/tests/TestFiles/Cairo/Shapes/Path/Line_Absolute.expected.png b/tests/TestFiles/Cairo/Shapes/Path/Line_Absolute.expected.png new file mode 100644 index 0000000000..4661a6a8c4 Binary files /dev/null and b/tests/TestFiles/Cairo/Shapes/Path/Line_Absolute.expected.png differ diff --git a/tests/TestFiles/Cairo/Shapes/Path/VerticalLine_Absolute.expected.png b/tests/TestFiles/Cairo/Shapes/Path/VerticalLine_Absolute.expected.png new file mode 100644 index 0000000000..b2e2beafd4 Binary files /dev/null and b/tests/TestFiles/Cairo/Shapes/Path/VerticalLine_Absolute.expected.png differ diff --git a/tests/TestFiles/Cairo/Shapes/Path/VerticalLine_Relative.expected.png b/tests/TestFiles/Cairo/Shapes/Path/VerticalLine_Relative.expected.png new file mode 100644 index 0000000000..ff36de2a5c Binary files /dev/null and b/tests/TestFiles/Cairo/Shapes/Path/VerticalLine_Relative.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Shapes/Path/Arc_Absolute.expected.png b/tests/TestFiles/Direct2D1/Shapes/Path/Arc_Absolute.expected.png new file mode 100644 index 0000000000..113cc908e6 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Shapes/Path/Arc_Absolute.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Shapes/Path/Arc_Relative.expected.png b/tests/TestFiles/Direct2D1/Shapes/Path/Arc_Relative.expected.png new file mode 100644 index 0000000000..113cc908e6 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Shapes/Path/Arc_Relative.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Shapes/Path/CubicBezier_Absolute.expected.png b/tests/TestFiles/Direct2D1/Shapes/Path/CubicBezier_Absolute.expected.png new file mode 100644 index 0000000000..8c8f5350ee Binary files /dev/null and b/tests/TestFiles/Direct2D1/Shapes/Path/CubicBezier_Absolute.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Shapes/Path/CubicBezier_Relative.expected.png b/tests/TestFiles/Direct2D1/Shapes/Path/CubicBezier_Relative.expected.png new file mode 100644 index 0000000000..8c8f5350ee Binary files /dev/null and b/tests/TestFiles/Direct2D1/Shapes/Path/CubicBezier_Relative.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Shapes/Path/HorizontalLine_Absolute.expected.png b/tests/TestFiles/Direct2D1/Shapes/Path/HorizontalLine_Absolute.expected.png new file mode 100644 index 0000000000..546ffec3bb Binary files /dev/null and b/tests/TestFiles/Direct2D1/Shapes/Path/HorizontalLine_Absolute.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Shapes/Path/HorizontalLine_Relative.expected.png b/tests/TestFiles/Direct2D1/Shapes/Path/HorizontalLine_Relative.expected.png new file mode 100644 index 0000000000..546ffec3bb Binary files /dev/null and b/tests/TestFiles/Direct2D1/Shapes/Path/HorizontalLine_Relative.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Shapes/Path/Line_Absolute.expected.png b/tests/TestFiles/Direct2D1/Shapes/Path/Line_Absolute.expected.png new file mode 100644 index 0000000000..4661a6a8c4 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Shapes/Path/Line_Absolute.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Shapes/Path/Line_Relative.expected.png b/tests/TestFiles/Direct2D1/Shapes/Path/Line_Relative.expected.png new file mode 100644 index 0000000000..4661a6a8c4 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Shapes/Path/Line_Relative.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Shapes/Path/VerticalLine_Absolute.expected.png b/tests/TestFiles/Direct2D1/Shapes/Path/VerticalLine_Absolute.expected.png new file mode 100644 index 0000000000..b2e2beafd4 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Shapes/Path/VerticalLine_Absolute.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Shapes/Path/VerticalLine_Relative.expected.png b/tests/TestFiles/Direct2D1/Shapes/Path/VerticalLine_Relative.expected.png new file mode 100644 index 0000000000..ff36de2a5c Binary files /dev/null and b/tests/TestFiles/Direct2D1/Shapes/Path/VerticalLine_Relative.expected.png differ diff --git a/tests/TestFiles/Skia/Shapes/Path/Arc_Absolute.expected.png b/tests/TestFiles/Skia/Shapes/Path/Arc_Absolute.expected.png new file mode 100644 index 0000000000..113cc908e6 Binary files /dev/null and b/tests/TestFiles/Skia/Shapes/Path/Arc_Absolute.expected.png differ diff --git a/tests/TestFiles/Skia/Shapes/Path/Arc_Relative.expected.png b/tests/TestFiles/Skia/Shapes/Path/Arc_Relative.expected.png new file mode 100644 index 0000000000..113cc908e6 Binary files /dev/null and b/tests/TestFiles/Skia/Shapes/Path/Arc_Relative.expected.png differ diff --git a/tests/TestFiles/Skia/Shapes/Path/CubicBezier_Absolute.expected.png b/tests/TestFiles/Skia/Shapes/Path/CubicBezier_Absolute.expected.png new file mode 100644 index 0000000000..8c8f5350ee Binary files /dev/null and b/tests/TestFiles/Skia/Shapes/Path/CubicBezier_Absolute.expected.png differ diff --git a/tests/TestFiles/Skia/Shapes/Path/CubicBezier_Relative.expected.png b/tests/TestFiles/Skia/Shapes/Path/CubicBezier_Relative.expected.png new file mode 100644 index 0000000000..8c8f5350ee Binary files /dev/null and b/tests/TestFiles/Skia/Shapes/Path/CubicBezier_Relative.expected.png differ diff --git a/tests/TestFiles/Skia/Shapes/Path/HorizontalLine_Absolute.expected.png b/tests/TestFiles/Skia/Shapes/Path/HorizontalLine_Absolute.expected.png new file mode 100644 index 0000000000..546ffec3bb Binary files /dev/null and b/tests/TestFiles/Skia/Shapes/Path/HorizontalLine_Absolute.expected.png differ diff --git a/tests/TestFiles/Skia/Shapes/Path/HorizontalLine_Relative.expected.png b/tests/TestFiles/Skia/Shapes/Path/HorizontalLine_Relative.expected.png new file mode 100644 index 0000000000..546ffec3bb Binary files /dev/null and b/tests/TestFiles/Skia/Shapes/Path/HorizontalLine_Relative.expected.png differ diff --git a/tests/TestFiles/Skia/Shapes/Path/Line_Absolute.expected.png b/tests/TestFiles/Skia/Shapes/Path/Line_Absolute.expected.png new file mode 100644 index 0000000000..4661a6a8c4 Binary files /dev/null and b/tests/TestFiles/Skia/Shapes/Path/Line_Absolute.expected.png differ diff --git a/tests/TestFiles/Skia/Shapes/Path/Line_Relative.expected.png b/tests/TestFiles/Skia/Shapes/Path/Line_Relative.expected.png new file mode 100644 index 0000000000..4661a6a8c4 Binary files /dev/null and b/tests/TestFiles/Skia/Shapes/Path/Line_Relative.expected.png differ diff --git a/tests/TestFiles/Skia/Shapes/Path/VerticalLine_Absolute.expected.png b/tests/TestFiles/Skia/Shapes/Path/VerticalLine_Absolute.expected.png new file mode 100644 index 0000000000..b2e2beafd4 Binary files /dev/null and b/tests/TestFiles/Skia/Shapes/Path/VerticalLine_Absolute.expected.png differ diff --git a/tests/TestFiles/Skia/Shapes/Path/VerticalLine_Relative.expected.png b/tests/TestFiles/Skia/Shapes/Path/VerticalLine_Relative.expected.png new file mode 100644 index 0000000000..ff36de2a5c Binary files /dev/null and b/tests/TestFiles/Skia/Shapes/Path/VerticalLine_Relative.expected.png differ