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..46e24aa15a 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,12 @@ + + + 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..3f4b405de9 100644 --- a/src/Avalonia.Base/PriorityValue.cs +++ b/src/Avalonia.Base/PriorityValue.cs @@ -28,7 +28,7 @@ namespace Avalonia { private readonly IPriorityValueOwner _owner; private readonly Type _valueType; - private readonly Dictionary _levels = new Dictionary(); + private readonly SingleOrDictionary _levels = new SingleOrDictionary(); private object _value; private readonly Func _validate; @@ -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/SingleOrDictionary.cs b/src/Avalonia.Base/Utilities/SingleOrDictionary.cs new file mode 100644 index 0000000000..fd984d758a --- /dev/null +++ b/src/Avalonia.Base/Utilities/SingleOrDictionary.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Avalonia.Utilities +{ + /// + /// Stores either a single key value pair or constructs a dictionary when more than one value is stored. + /// + /// The type of the key. + /// The type of the value. + public class SingleOrDictionary : IEnumerable> + { + private KeyValuePair? _singleValue; + private Dictionary dictionary; + + public void Add(TKey key, TValue value) + { + if (_singleValue != null) + { + dictionary = new Dictionary(); + ((ICollection>)dictionary).Add(_singleValue.Value); + _singleValue = null; + } + + if (dictionary != null) + { + dictionary.Add(key, value); + } + else + { + _singleValue = new KeyValuePair(key, value); + } + } + + public bool TryGetValue(TKey key, out TValue value) + { + if (dictionary == null) + { + if (!_singleValue.HasValue || !_singleValue.Value.Key.Equals(key)) + { + value = default(TValue); + return false; + } + else + { + value = _singleValue.Value.Value; + return true; + } + } + else + { + return dictionary.TryGetValue(key, out value); + } + } + + public IEnumerator> GetEnumerator() + { + if (dictionary == null) + { + if (_singleValue.HasValue) + { + return new SingleEnumerator>(_singleValue.Value); + } + } + else + { + return dictionary.GetEnumerator(); + } + return Enumerable.Empty>().GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public IEnumerable Values + { + get + { + if(dictionary == null) + { + if (_singleValue.HasValue) + { + return new[] { _singleValue.Value.Value }; + } + } + else + { + return dictionary.Values; + } + return Enumerable.Empty(); + } + } + + private class SingleEnumerator : IEnumerator + { + private T value; + private int index = -1; + + public SingleEnumerator(T value) + { + this.value = value; + } + + public T Current + { + get + { + if (index == 0) + { + return value; + } + else + { + throw new InvalidOperationException(); + } + } + } + + object IEnumerator.Current => Current; + + public void Dispose() + { + } + + public bool MoveNext() + { + index++; + return index < 1; + } + + public void Reset() + { + index = -1; + } + } + + } +} 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 ae3058e578..336f7da2dc 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -43,7 +43,7 @@ Properties\SharedAssemblyInfo.cs - + @@ -57,6 +57,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/IVirtualizingPanel.cs b/src/Avalonia.Controls/IVirtualizingPanel.cs index 5d35fa1ec8..792dee8ae8 100644 --- a/src/Avalonia.Controls/IVirtualizingPanel.cs +++ b/src/Avalonia.Controls/IVirtualizingPanel.cs @@ -1,6 +1,8 @@ // 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.Layout; + namespace Avalonia.Controls { /// @@ -24,8 +26,8 @@ namespace Avalonia.Controls /// /// /// This property should return false until enough children are added to fill the space - /// passed into the last measure in the direction of scroll. It should be updated - /// immediately after a child is added or removed. + /// passed into the last measure or arrange in the direction of scroll. It should be + /// updated immediately after a child is added or removed. /// bool IsFull { get; } @@ -63,5 +65,18 @@ namespace Avalonia.Controls /// Gets or sets the current pixel offset of the items in the direction of scroll. /// double PixelOffset { get; set; } + + /// + /// Invalidates the measure of the control and forces a call to + /// on the next measure. + /// + /// + /// The implementation for this method should call + /// and also ensure that the next call to + /// calls + /// on the next measure even if + /// the available size hasn't changed. + /// + void ForceInvalidateMeasure(); } } 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/ITopLevelImpl.cs b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs index 281851a512..087a333e8a 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs @@ -22,6 +22,11 @@ namespace Avalonia.Platform /// Size ClientSize { get; set; } + /// + /// Gets the maximum size of a window on the system. + /// + Size MaxClientSize { get; } + /// /// Gets the scaling factor for the window. /// diff --git a/src/Avalonia.Controls/Platform/IWindowImpl.cs b/src/Avalonia.Controls/Platform/IWindowImpl.cs index 05a51d02aa..609e9834cb 100644 --- a/src/Avalonia.Controls/Platform/IWindowImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowImpl.cs @@ -11,11 +11,6 @@ namespace Avalonia.Platform /// public interface IWindowImpl : ITopLevelImpl { - /// - /// Gets the maximum size of a window on the system. - /// - Size MaxClientSize { get; } - /// /// Gets or sets the minimized/maximized state of the window. /// 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/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 83a0313dd5..d2e5a720b7 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -121,6 +121,7 @@ namespace Avalonia.Controls.Presenters RecycleContainers(); } + panel.ForceInvalidateMeasure(); break; case NotifyCollectionChangedAction.Remove: @@ -130,6 +131,7 @@ namespace Avalonia.Controls.Presenters RecycleContainersOnRemove(); } + panel.ForceInvalidateMeasure(); break; case NotifyCollectionChangedAction.Move: @@ -140,6 +142,7 @@ namespace Avalonia.Controls.Presenters case NotifyCollectionChangedAction.Reset: RecycleContainersOnRemove(); CreateAndRemoveContainers(); + panel.ForceInvalidateMeasure(); break; } } diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 2e586a25e3..fa15766e4d 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -99,7 +99,7 @@ namespace Avalonia.Controls.Presenters // the available size. if (availableSize == Size.Infinity && VirtualizationMode != ItemVirtualizationMode.None) { - var window = VisualRoot as Window; + var window = VisualRoot as TopLevel; if (window != null) { diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index bb54ac4a39..f9c55e1f3b 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -106,6 +106,11 @@ namespace Avalonia.Controls.Presenters { var start = Math.Min(selectionStart, selectionEnd); var length = Math.Max(selectionStart, selectionEnd) - start; + + // issue #600: set constaint before any FormattedText manipulation + // see base.Render(...) implementation + FormattedText.Constraint = Bounds.Size; + var rects = FormattedText.HitTestTextRange(start, length); if (_highlightBrush == null) 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.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 6daad0b90e..2e5afaf170 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -19,6 +19,7 @@ namespace Avalonia.Controls private double _averageItemSize; private int _averageCount; private double _pixelOffset; + private bool _forceRemeasure; bool IVirtualizingPanel.IsFull { @@ -61,10 +62,17 @@ namespace Avalonia.Controls private IVirtualizingController Controller => ((IVirtualizingPanel)this).Controller; + void IVirtualizingPanel.ForceInvalidateMeasure() + { + InvalidateMeasure(); + _forceRemeasure = true; + } + protected override Size MeasureOverride(Size availableSize) { - if (availableSize != ((ILayoutable)this).PreviousMeasure) + if (_forceRemeasure || availableSize != ((ILayoutable)this).PreviousMeasure) { + _forceRemeasure = false; _availableSpace = availableSize; Controller?.UpdateControls(); } @@ -74,6 +82,7 @@ namespace Avalonia.Controls protected override Size ArrangeOverride(Size finalSize) { + _availableSpace = finalSize; _canBeRemoved = 0; _takenSpace = 0; _averageItemSize = 0; 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 87f733b370..bc60e5eb65 100644 --- a/src/Gtk/Avalonia.Gtk/WindowImpl.cs +++ b/src/Gtk/Avalonia.Gtk/WindowImpl.cs @@ -1,165 +1,78 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - using System; using System.Reactive.Disposables; -using System.Runtime.InteropServices; -using Gdk; -using Avalonia.Controls; -using Avalonia.Input.Raw; using Avalonia.Platform; -using Avalonia.Input; -using Avalonia.Threading; -using Action = System.Action; -using WindowEdge = Avalonia.Controls.WindowEdge; +using Gdk; namespace Avalonia.Gtk { using Gtk = global::Gtk; - - public class WindowImpl : Gtk.Window, IWindowImpl, IPlatformHandle + public class WindowImpl : WindowImplBase { - private IInputRoot _inputRoot; + private Gtk.Window _window; + private Gtk.Window Window => _window ?? (_window = (Gtk.Window) Widget); - private Size _lastClientSize; +<<<<<<< HEAD private Point _lastPosition; private Gtk.IMContext _imContext; +======= +>>>>>>> dfd4bbf34a7632465a84126b22495072945fa7d5 - private uint _lastKeyEventTimestamp; - - private static readonly Gdk.Cursor DefaultCursor = new Gdk.Cursor(CursorType.LeftPtr); - - public WindowImpl() - : base(Gtk.WindowType.Toplevel) + public WindowImpl(Gtk.WindowType type) : base(new PlatformHandleAwareWindow(type)) { - DefaultSize = new Gdk.Size(900, 480); Init(); } - public WindowImpl(Gtk.WindowType type) - : base(type) + public WindowImpl() + : base(new PlatformHandleAwareWindow(Gtk.WindowType.Toplevel) {DefaultSize = new Gdk.Size(900, 480)}) { Init(); } - private void Init() + void Init() { - Events = EventMask.PointerMotionMask | - EventMask.ButtonPressMask | - EventMask.ButtonReleaseMask; - _imContext = new Gtk.IMMulticontext(); - _imContext.Commit += ImContext_Commit; - DoubleBuffered = false; - Realize(); + Window.FocusActivated += OnFocusActivated; + Window.ConfigureEvent += OnConfigureEvent; _lastClientSize = ClientSize; _lastPosition = Position; } - - protected override void OnRealized () - { - base.OnRealized (); - _imContext.ClientWindow = this.GdkWindow; - } - - public Size ClientSize + private Size _lastClientSize; + void OnConfigureEvent(object o, Gtk.ConfigureEventArgs args) { - get - { - int width; - int height; - GetSize(out width, out height); - return new Size(width, height); - } - - set - { - Resize((int)value.Width, (int)value.Height); - } - } + var evnt = args.Event; + args.RetVal = true; + var newSize = new Size(evnt.Width, evnt.Height); - public Size MaxClientSize - { - get + if (newSize != _lastClientSize) { - // TODO: This should take into account things such as taskbar and window border - // thickness etc. - return new Size(Screen.Width, Screen.Height); + Resized(newSize); + _lastClientSize = newSize; } } - public Avalonia.Controls.WindowState WindowState + public override Size ClientSize { get { - switch (GdkWindow.State) - { - case Gdk.WindowState.Iconified: - return Controls.WindowState.Minimized; - case Gdk.WindowState.Maximized: - return Controls.WindowState.Maximized; - default: - return Controls.WindowState.Normal; - } + int width; + int height; + Window.GetSize(out width, out height); + return new Size(width, height); } set { - switch (value) - { - case Controls.WindowState.Minimized: - GdkWindow.Iconify(); - break; - case Controls.WindowState.Maximized: - GdkWindow.Maximize(); - break; - case Controls.WindowState.Normal: - GdkWindow.Deiconify(); - GdkWindow.Unmaximize(); - break; - } + Window.Resize((int)value.Width, (int)value.Height); } } - public double Scaling => 1; - - IPlatformHandle ITopLevelImpl.Handle => this; - - [DllImport("libgdk-win32-2.0-0.dll", CallingConvention = CallingConvention.Cdecl)] - extern static IntPtr gdk_win32_drawable_get_handle(IntPtr gdkWindow); - - [DllImport("libgtk-x11-2.0.so.0", CallingConvention = CallingConvention.Cdecl)] - extern static IntPtr gdk_x11_drawable_get_xid(IntPtr gdkWindow); - - [DllImport("libgdk-quartz-2.0-0.dylib", CallingConvention = CallingConvention.Cdecl)] - extern static IntPtr gdk_quartz_window_get_nswindow(IntPtr gdkWindow); - - IntPtr _nativeWindow; - - IntPtr GetNativeWindow() + public override void SetTitle(string title) { - IntPtr h = GdkWindow.Handle; - if (_nativeWindow != IntPtr.Zero) - return _nativeWindow; - //Try whatever backend that works - try - { - return _nativeWindow = gdk_quartz_window_get_nswindow(h); - } - catch - { - } - try - { - return _nativeWindow = gdk_x11_drawable_get_xid(h); - } - catch - { - } - return _nativeWindow = gdk_win32_drawable_get_handle(h); + Window.Title = title; } +<<<<<<< HEAD IntPtr IPlatformHandle.Handle => GetNativeWindow(); public string HandleDescriptor => "HWND"; @@ -193,75 +106,52 @@ namespace Avalonia.Gtk } public Point PointToClient(Point point) +======= + void OnFocusActivated(object sender, EventArgs eventArgs) +>>>>>>> dfd4bbf34a7632465a84126b22495072945fa7d5 { - int x, y; - GdkWindow.GetDeskrelativeOrigin(out x, out y); - - return new Point(point.X - x, point.Y - y); - } - - public Point PointToScreen(Point point) - { - int x, y; - GdkWindow.GetDeskrelativeOrigin(out x, out y); - - return new Point(point.X + x, point.Y + y); - } - - public void SetInputRoot(IInputRoot inputRoot) - { - _inputRoot = inputRoot; - } - - public void SetTitle(string title) - { - Title = title; - } - - - public void SetCursor(IPlatformHandle cursor) - { - GdkWindow.Cursor = cursor != null ? new Gdk.Cursor(cursor.Handle) : DefaultCursor; + Activated(); } - public void BeginMoveDrag() + public override void BeginMoveDrag() { int x, y; ModifierType mod; - Screen.RootWindow.GetPointer(out x, out y, out mod); - BeginMoveDrag(1, x, y, 0); + Window.Screen.RootWindow.GetPointer(out x, out y, out mod); + Window.BeginMoveDrag(1, x, y, 0); } - public void BeginResizeDrag(WindowEdge edge) + public override void BeginResizeDrag(Controls.WindowEdge edge) { int x, y; ModifierType mod; - Screen.RootWindow.GetPointer(out x, out y, out mod); - BeginResizeDrag((Gdk.WindowEdge) (int) edge, 1, x, y, 0); + Window.Screen.RootWindow.GetPointer(out x, out y, out mod); + Window.BeginResizeDrag((Gdk.WindowEdge)(int)edge, 1, x, y, 0); } - public Point Position + public override Point Position { get { int x, y; - GetPosition(out x, out y); + Window.GetPosition(out x, out y); return new Point(x, y); } set { - Move((int)value.X, (int)value.Y); + Window.Move((int)value.X, (int)value.Y); } } - public IDisposable ShowDialog() + public override IDisposable ShowDialog() { - Modal = true; - Show(); + Window.Modal = true; + Window.Show(); return Disposable.Empty; } +<<<<<<< HEAD public void SetSystemDecorations(bool enabled) => Decorated = enabled; void ITopLevelImpl.Activate() @@ -408,10 +298,13 @@ namespace Avalonia.Gtk Input(e); return true; } +======= + public override void SetSystemDecorations(bool enabled) => Window.Decorated = enabled; +>>>>>>> dfd4bbf34a7632465a84126b22495072945fa7d5 - public void SetIcon(IWindowIconImpl icon) + public override void SetIcon(IWindowIconImpl icon) { - Icon = ((IconImpl)icon).Pixbuf; + Window.Icon = ((IconImpl)icon).Pixbuf; } } -} +} \ No newline at end of file diff --git a/src/Gtk/Avalonia.Gtk/WindowImplBase.cs b/src/Gtk/Avalonia.Gtk/WindowImplBase.cs new file mode 100644 index 0000000000..759d35b1fc --- /dev/null +++ b/src/Gtk/Avalonia.Gtk/WindowImplBase.cs @@ -0,0 +1,312 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Reactive.Disposables; +using System.Runtime.InteropServices; +using Gdk; +using Avalonia.Controls; +using Avalonia.Input.Raw; +using Avalonia.Platform; +using Avalonia.Input; +using Avalonia.Threading; +using Action = System.Action; +using WindowEdge = Avalonia.Controls.WindowEdge; + +namespace Avalonia.Gtk +{ + using Gtk = global::Gtk; + + public abstract class WindowImplBase : IWindowImpl + { + private IInputRoot _inputRoot; + protected Gtk.Widget _window; + public Gtk.Widget Widget => _window; + + + private Gtk.IMContext _imContext; + + private uint _lastKeyEventTimestamp; + + private static readonly Gdk.Cursor DefaultCursor = new Gdk.Cursor(CursorType.LeftPtr); + + protected WindowImplBase(Gtk.Widget window) + { + _window = window; + Init(); + } + + void Init() + { + Handle = _window as IPlatformHandle; + _window.Events = EventMask.AllEventsMask; + _imContext = new Gtk.IMMulticontext(); + _imContext.Commit += ImContext_Commit; + _window.Realized += OnRealized; + _window.DoubleBuffered = false; + _window.Realize(); + _window.ButtonPressEvent += OnButtonPressEvent; + _window.ButtonReleaseEvent += OnButtonReleaseEvent; + _window.ScrollEvent += OnScrollEvent; + _window.Destroyed += OnDestroyed; + _window.KeyPressEvent += OnKeyPressEvent; + _window.KeyReleaseEvent += OnKeyReleaseEvent; + _window.ExposeEvent += OnExposeEvent; + _window.MotionNotifyEvent += OnMotionNotifyEvent; + + } + + public IPlatformHandle Handle { get; private set; } + + void OnRealized (object sender, EventArgs eventArgs) + { + _imContext.ClientWindow = _window.GdkWindow; + } + + public abstract Size ClientSize { get; set; } + + + public Size MaxClientSize + { + get + { + // TODO: This should take into account things such as taskbar and window border + // thickness etc. + return new Size(_window.Screen.Width, _window.Screen.Height); + } + } + + public Avalonia.Controls.WindowState WindowState + { + get + { + switch (_window.GdkWindow.State) + { + case Gdk.WindowState.Iconified: + return Controls.WindowState.Minimized; + case Gdk.WindowState.Maximized: + return Controls.WindowState.Maximized; + default: + return Controls.WindowState.Normal; + } + } + + set + { + switch (value) + { + case Controls.WindowState.Minimized: + _window.GdkWindow.Iconify(); + break; + case Controls.WindowState.Maximized: + _window.GdkWindow.Maximize(); + break; + case Controls.WindowState.Normal: + _window.GdkWindow.Deiconify(); + _window.GdkWindow.Unmaximize(); + break; + } + } + } + + public double Scaling => 1; + + public Action Activated { get; set; } + + public Action Closed { get; set; } + + public Action Deactivated { get; set; } + + public Action Input { get; set; } + + public Action Paint { get; set; } + + public Action Resized { get; set; } + + public Action PositionChanged { get; set; } + + public Action ScalingChanged { get; set; } + + public IPopupImpl CreatePopup() + { + return new PopupImpl(); + } + + public void Invalidate(Rect rect) + { + if (_window.GdkWindow != null) + _window.GdkWindow.InvalidateRect( + new Rectangle((int) rect.X, (int) rect.Y, (int) rect.Width, (int) rect.Height), true); + } + + public Point PointToClient(Point point) + { + int x, y; + _window.GdkWindow.GetDeskrelativeOrigin(out x, out y); + + return new Point(point.X - x, point.Y - y); + } + + public Point PointToScreen(Point point) + { + int x, y; + _window.GdkWindow.GetDeskrelativeOrigin(out x, out y); + return new Point(point.X + x, point.Y + y); + } + + public void SetInputRoot(IInputRoot inputRoot) + { + _inputRoot = inputRoot; + } + + public abstract void SetTitle(string title); + public abstract IDisposable ShowDialog(); + public abstract void SetSystemDecorations(bool enabled); + public abstract void SetIcon(IWindowIconImpl icon); + + + public void SetCursor(IPlatformHandle cursor) + { + _window.GdkWindow.Cursor = cursor != null ? new Gdk.Cursor(cursor.Handle) : DefaultCursor; + } + + public void Show() => _window.Show(); + + public void Hide() => _window.Hide(); + public abstract void BeginMoveDrag(); + public abstract void BeginResizeDrag(WindowEdge edge); + public abstract Point Position { get; set; } + + void ITopLevelImpl.Activate() + { + _window.Activate(); + } + + private static InputModifiers GetModifierKeys(ModifierType state) + { + var rv = InputModifiers.None; + if (state.HasFlag(ModifierType.ControlMask)) + rv |= InputModifiers.Control; + if (state.HasFlag(ModifierType.ShiftMask)) + rv |= InputModifiers.Shift; + if (state.HasFlag(ModifierType.Mod1Mask)) + rv |= InputModifiers.Control; + if(state.HasFlag(ModifierType.Button1Mask)) + rv |= InputModifiers.LeftMouseButton; + if (state.HasFlag(ModifierType.Button2Mask)) + rv |= InputModifiers.RightMouseButton; + if (state.HasFlag(ModifierType.Button3Mask)) + rv |= InputModifiers.MiddleMouseButton; + return rv; + } + + void OnButtonPressEvent(object o, Gtk.ButtonPressEventArgs args) + { + var evnt = args.Event; + var e = new RawMouseEventArgs( + GtkMouseDevice.Instance, + evnt.Time, + _inputRoot, + evnt.Button == 1 + ? RawMouseEventType.LeftButtonDown + : evnt.Button == 3 ? RawMouseEventType.RightButtonDown : RawMouseEventType.MiddleButtonDown, + new Point(evnt.X, evnt.Y), GetModifierKeys(evnt.State)); + Input(e); + } + + void OnScrollEvent(object o, Gtk.ScrollEventArgs args) + { + var evnt = args.Event; + double step = 1; + var delta = new Vector(); + if (evnt.Direction == ScrollDirection.Down) + delta = new Vector(0, -step); + else if (evnt.Direction == ScrollDirection.Up) + delta = new Vector(0, step); + else if (evnt.Direction == ScrollDirection.Right) + delta = new Vector(-step, 0); + if (evnt.Direction == ScrollDirection.Left) + delta = new Vector(step, 0); + var e = new RawMouseWheelEventArgs(GtkMouseDevice.Instance, evnt.Time, _inputRoot, new Point(evnt.X, evnt.Y), delta, GetModifierKeys(evnt.State)); + Input(e); + } + + protected void OnButtonReleaseEvent(object o, Gtk.ButtonReleaseEventArgs args) + { + var evnt = args.Event; + var e = new RawMouseEventArgs( + GtkMouseDevice.Instance, + evnt.Time, + _inputRoot, + evnt.Button == 1 + ? RawMouseEventType.LeftButtonUp + : evnt.Button == 3 ? RawMouseEventType.RightButtonUp : RawMouseEventType.MiddleButtonUp, + new Point(evnt.X, evnt.Y), GetModifierKeys(evnt.State)); + Input(e); + } + + void OnDestroyed(object sender, EventArgs eventArgs) + { + Closed(); + } + + private void ProcessKeyEvent(EventKey evnt) + { + + _lastKeyEventTimestamp = evnt.Time; + if (_imContext.FilterKeypress(evnt)) + return; + var e = new RawKeyEventArgs( + GtkKeyboardDevice.Instance, + evnt.Time, + evnt.Type == EventType.KeyPress ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp, + GtkKeyboardDevice.ConvertKey(evnt.Key), GetModifierKeys(evnt.State)); + Input(e); + } + + void OnKeyPressEvent(object o, Gtk.KeyPressEventArgs args) + { + args.RetVal = true; + ProcessKeyEvent(args.Event); + } + + void OnKeyReleaseEvent(object o, Gtk.KeyReleaseEventArgs args) + { + args.RetVal = true; + ProcessKeyEvent(args.Event); + } + + private void ImContext_Commit(object o, Gtk.CommitArgs args) + { + Input(new RawTextInputEventArgs(GtkKeyboardDevice.Instance, _lastKeyEventTimestamp, args.Str)); + } + + void OnExposeEvent(object o, Gtk.ExposeEventArgs args) + { + Paint(args.Event.Area.ToAvalonia()); + args.RetVal = true; + } + + void OnMotionNotifyEvent(object o, Gtk.MotionNotifyEventArgs args) + { + var evnt = args.Event; + var position = new Point(evnt.X, evnt.Y); + + GtkMouseDevice.Instance.SetClientPosition(position); + + var e = new RawMouseEventArgs( + GtkMouseDevice.Instance, + evnt.Time, + _inputRoot, + RawMouseEventType.Move, + position, GetModifierKeys(evnt.State)); + Input(e); + args.RetVal = true; + } + + public void Dispose() + { + _window.Dispose(); + } + } +} diff --git a/src/Gtk/Avalonia.Gtk/WindowImpl_BASE_46644.cs.orig b/src/Gtk/Avalonia.Gtk/WindowImpl_BASE_46644.cs.orig new file mode 100644 index 0000000000..26ad52135d --- /dev/null +++ b/src/Gtk/Avalonia.Gtk/WindowImpl_BASE_46644.cs.orig @@ -0,0 +1,404 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Reactive.Disposables; +using System.Runtime.InteropServices; +using Gdk; +using Avalonia.Controls; +using Avalonia.Input.Raw; +using Avalonia.Platform; +using Avalonia.Input; +using Avalonia.Threading; +using Action = System.Action; +using WindowEdge = Avalonia.Controls.WindowEdge; + +namespace Avalonia.Gtk +{ + using Gtk = global::Gtk; + + public class WindowImpl : Gtk.Window, IWindowImpl, IPlatformHandle + { + private IInputRoot _inputRoot; + + private Size _lastClientSize; + + private Gtk.IMContext _imContext; + + private uint _lastKeyEventTimestamp; + + private static readonly Gdk.Cursor DefaultCursor = new Gdk.Cursor(CursorType.LeftPtr); + + public WindowImpl() + : base(Gtk.WindowType.Toplevel) + { + DefaultSize = new Gdk.Size(900, 480); + Init(); + } + + public WindowImpl(Gtk.WindowType type) + : base(type) + { + Init(); + } + + private void Init() + { + Events = EventMask.PointerMotionMask | + EventMask.ButtonPressMask | + EventMask.ButtonReleaseMask; + _imContext = new Gtk.IMMulticontext(); + _imContext.Commit += ImContext_Commit; + DoubleBuffered = false; + Realize(); + _lastClientSize = ClientSize; + } + + protected override void OnRealized () + { + base.OnRealized (); + _imContext.ClientWindow = this.GdkWindow; + } + + public Size ClientSize + { + get + { + int width; + int height; + GetSize(out width, out height); + return new Size(width, height); + } + + set + { + Resize((int)value.Width, (int)value.Height); + } + } + + public Size MaxClientSize + { + get + { + // TODO: This should take into account things such as taskbar and window border + // thickness etc. + return new Size(Screen.Width, Screen.Height); + } + } + + public Avalonia.Controls.WindowState WindowState + { + get + { + switch (GdkWindow.State) + { + case Gdk.WindowState.Iconified: + return Controls.WindowState.Minimized; + case Gdk.WindowState.Maximized: + return Controls.WindowState.Maximized; + default: + return Controls.WindowState.Normal; + } + } + + set + { + switch (value) + { + case Controls.WindowState.Minimized: + GdkWindow.Iconify(); + break; + case Controls.WindowState.Maximized: + GdkWindow.Maximize(); + break; + case Controls.WindowState.Normal: + GdkWindow.Deiconify(); + GdkWindow.Unmaximize(); + break; + } + } + } + + public double Scaling => 1; + + IPlatformHandle ITopLevelImpl.Handle => this; + + [DllImport("libgdk-win32-2.0-0.dll", CallingConvention = CallingConvention.Cdecl)] + extern static IntPtr gdk_win32_drawable_get_handle(IntPtr gdkWindow); + + [DllImport("libgtk-x11-2.0.so.0", CallingConvention = CallingConvention.Cdecl)] + extern static IntPtr gdk_x11_drawable_get_xid(IntPtr gdkWindow); + + [DllImport("libgdk-quartz-2.0-0.dylib", CallingConvention = CallingConvention.Cdecl)] + extern static IntPtr gdk_quartz_window_get_nswindow(IntPtr gdkWindow); + + IntPtr _nativeWindow; + + IntPtr GetNativeWindow() + { + IntPtr h = GdkWindow.Handle; + if (_nativeWindow != IntPtr.Zero) + return _nativeWindow; + //Try whatever backend that works + try + { + return _nativeWindow = gdk_quartz_window_get_nswindow(h); + } + catch + { + } + try + { + return _nativeWindow = gdk_x11_drawable_get_xid(h); + } + catch + { + } + return _nativeWindow = gdk_win32_drawable_get_handle(h); + } + + + IntPtr IPlatformHandle.Handle => GetNativeWindow(); + public string HandleDescriptor => "HWND"; + + public Action Activated { get; set; } + + public Action Closed { get; set; } + + public Action Deactivated { get; set; } + + public Action Input { get; set; } + + public Action Paint { get; set; } + + public Action Resized { get; set; } + + public Action ScalingChanged { get; set; } + + public IPopupImpl CreatePopup() + { + return new PopupImpl(); + } + + public void Invalidate(Rect rect) + { + if (base.GdkWindow != null) + base.GdkWindow.InvalidateRect( + new Rectangle((int) rect.X, (int) rect.Y, (int) rect.Width, (int) rect.Height), true); + } + + public Point PointToClient(Point point) + { + int x, y; + GdkWindow.GetDeskrelativeOrigin(out x, out y); + + return new Point(point.X - x, point.Y - y); + } + + public Point PointToScreen(Point point) + { + int x, y; + GdkWindow.GetDeskrelativeOrigin(out x, out y); + + return new Point(point.X + x, point.Y + y); + } + + public void SetInputRoot(IInputRoot inputRoot) + { + _inputRoot = inputRoot; + } + + public void SetTitle(string title) + { + Title = title; + } + + + public void SetCursor(IPlatformHandle cursor) + { + GdkWindow.Cursor = cursor != null ? new Gdk.Cursor(cursor.Handle) : DefaultCursor; + } + + public void BeginMoveDrag() + { + int x, y; + ModifierType mod; + Screen.RootWindow.GetPointer(out x, out y, out mod); + BeginMoveDrag(1, x, y, 0); + } + + public void BeginResizeDrag(WindowEdge edge) + { + int x, y; + ModifierType mod; + Screen.RootWindow.GetPointer(out x, out y, out mod); + BeginResizeDrag((Gdk.WindowEdge) (int) edge, 1, x, y, 0); + } + + public Point Position + { + get + { + int x, y; + GetPosition(out x, out y); + return new Point(x, y); + } + set + { + Move((int)value.X, (int)value.Y); + } + } + + public IDisposable ShowDialog() + { + Modal = true; + Show(); + + return Disposable.Empty; + } + + public void SetSystemDecorations(bool enabled) => Decorated = enabled; + + void ITopLevelImpl.Activate() + { + Activate(); + } + + private static InputModifiers GetModifierKeys(ModifierType state) + { + var rv = InputModifiers.None; + if (state.HasFlag(ModifierType.ControlMask)) + rv |= InputModifiers.Control; + if (state.HasFlag(ModifierType.ShiftMask)) + rv |= InputModifiers.Shift; + if (state.HasFlag(ModifierType.Mod1Mask)) + rv |= InputModifiers.Control; + if(state.HasFlag(ModifierType.Button1Mask)) + rv |= InputModifiers.LeftMouseButton; + if (state.HasFlag(ModifierType.Button2Mask)) + rv |= InputModifiers.RightMouseButton; + if (state.HasFlag(ModifierType.Button3Mask)) + rv |= InputModifiers.MiddleMouseButton; + return rv; + } + + protected override bool OnButtonPressEvent(EventButton evnt) + { + + var e = new RawMouseEventArgs( + GtkMouseDevice.Instance, + evnt.Time, + _inputRoot, + evnt.Button == 1 + ? RawMouseEventType.LeftButtonDown + : evnt.Button == 3 ? RawMouseEventType.RightButtonDown : RawMouseEventType.MiddleButtonDown, + new Point(evnt.X, evnt.Y), GetModifierKeys(evnt.State)); + Input(e); + return true; + } + + protected override bool OnScrollEvent(EventScroll evnt) + { + double step = 1; + var delta = new Vector(); + if (evnt.Direction == ScrollDirection.Down) + delta = new Vector(0, -step); + else if (evnt.Direction == ScrollDirection.Up) + delta = new Vector(0, step); + else if (evnt.Direction == ScrollDirection.Right) + delta = new Vector(-step, 0); + if (evnt.Direction == ScrollDirection.Left) + delta = new Vector(step, 0); + var e = new RawMouseWheelEventArgs(GtkMouseDevice.Instance, evnt.Time, _inputRoot, new Point(evnt.X, evnt.Y), delta, GetModifierKeys(evnt.State)); + Input(e); + return base.OnScrollEvent(evnt); + } + + protected override bool OnButtonReleaseEvent(EventButton evnt) + { + var e = new RawMouseEventArgs( + GtkMouseDevice.Instance, + evnt.Time, + _inputRoot, + evnt.Button == 1 + ? RawMouseEventType.LeftButtonUp + : evnt.Button == 3 ? RawMouseEventType.RightButtonUp : RawMouseEventType.MiddleButtonUp, + new Point(evnt.X, evnt.Y), GetModifierKeys(evnt.State)); + Input(e); + return true; + } + + protected override bool OnConfigureEvent(EventConfigure evnt) + { + var newSize = new Size(evnt.Width, evnt.Height); + + if (newSize != _lastClientSize) + { + Resized(newSize); + _lastClientSize = newSize; + } + + return true; + } + + protected override void OnDestroyed() + { + Closed(); + } + + private bool ProcessKeyEvent(EventKey evnt) + { + _lastKeyEventTimestamp = evnt.Time; + if (_imContext.FilterKeypress(evnt)) + return true; + var e = new RawKeyEventArgs( + GtkKeyboardDevice.Instance, + evnt.Time, + evnt.Type == EventType.KeyPress ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp, + GtkKeyboardDevice.ConvertKey(evnt.Key), GetModifierKeys(evnt.State)); + Input(e); + return true; + } + + protected override bool OnKeyPressEvent(EventKey evnt) => ProcessKeyEvent(evnt); + + protected override bool OnKeyReleaseEvent(EventKey evnt) => ProcessKeyEvent(evnt); + + private void ImContext_Commit(object o, Gtk.CommitArgs args) + { + Input(new RawTextInputEventArgs(GtkKeyboardDevice.Instance, _lastKeyEventTimestamp, args.Str)); + } + + protected override bool OnExposeEvent(EventExpose evnt) + { + Paint(evnt.Area.ToAvalonia()); + return true; + } + + protected override void OnFocusActivated() + { + Activated(); + } + + protected override bool OnMotionNotifyEvent(EventMotion evnt) + { + var position = new Point(evnt.X, evnt.Y); + + GtkMouseDevice.Instance.SetClientPosition(position); + + var e = new RawMouseEventArgs( + GtkMouseDevice.Instance, + evnt.Time, + _inputRoot, + RawMouseEventType.Move, + position, GetModifierKeys(evnt.State)); + Input(e); + return true; + } + + public void SetIcon(IWindowIconImpl icon) + { + Icon = ((IconImpl)icon).Pixbuf; + } + } +} diff --git a/src/Gtk/Avalonia.Gtk/Windows.cs b/src/Gtk/Avalonia.Gtk/Windows.cs new file mode 100644 index 0000000000..fe325f4114 --- /dev/null +++ b/src/Gtk/Avalonia.Gtk/Windows.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Platform; +using Gdk; +using Gtk; +using Window = Gtk.Window; +using WindowType = Gtk.WindowType; + +namespace Avalonia.Gtk +{ + class PlatformHandleAwareWindow : Window, IPlatformHandle + { + public PlatformHandleAwareWindow(WindowType type) : base(type) + { + Events = EventMask.AllEventsMask; + } + + IntPtr IPlatformHandle.Handle => GetNativeWindow(); + public string HandleDescriptor => "HWND"; + + + [DllImport("libgdk-win32-2.0-0.dll", CallingConvention = CallingConvention.Cdecl)] + static extern IntPtr gdk_win32_drawable_get_handle(IntPtr gdkWindow); + + [DllImport("libgtk-x11-2.0.so.0", CallingConvention = CallingConvention.Cdecl)] + static extern IntPtr gdk_x11_drawable_get_xid(IntPtr gdkWindow); + + [DllImport("libgdk-quartz-2.0-0.dylib", CallingConvention = CallingConvention.Cdecl)] + static extern IntPtr gdk_quartz_window_get_nswindow(IntPtr gdkWindow); + + IntPtr _nativeWindow; + + IntPtr GetNativeWindow() + { + if (_nativeWindow != IntPtr.Zero) + return _nativeWindow; + return _nativeWindow = GetNativeWindow(GdkWindow); + } + + public static IntPtr GetNativeWindow(Gdk.Window window) + { + IntPtr h = window.Handle; + + //Try whatever backend that works + try + { + return gdk_quartz_window_get_nswindow(h); + } + catch + { + } + try + { + return gdk_x11_drawable_get_xid(h); + } + catch + { + } + return gdk_win32_drawable_get_handle(h); + } + + protected override bool OnConfigureEvent(EventConfigure evnt) + { + base.OnConfigureEvent(evnt); + return false; + } + } + + class PlatformHandleAwareDrawingArea : DrawingArea, IPlatformHandle + { + + + + IntPtr IPlatformHandle.Handle => GetNativeWindow(); + public string HandleDescriptor => "HWND"; + IntPtr _nativeWindow; + + IntPtr GetNativeWindow() + { + + if (_nativeWindow != IntPtr.Zero) + return _nativeWindow; + Realize(); + return _nativeWindow = PlatformHandleAwareWindow.GetNativeWindow(GdkWindow); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs b/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs index ec60695374..086257f24c 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs @@ -27,10 +27,12 @@ namespace Avalonia.Markup.Xaml.Data /// Initializes a new instance of the class. /// /// The binding path. - public Binding(string path) + /// The binding mode. + public Binding(string path, BindingMode mode = BindingMode.Default) : this() { Path = path; + Mode = mode; } /// @@ -78,21 +80,18 @@ namespace Avalonia.Markup.Xaml.Data /// public object Source { get; set; } - /// - /// Gets or sets a value indicating whether the property should be validated. - /// - public bool EnableValidation { get; set; } - /// public InstancedBinding Initiate( IAvaloniaObject target, AvaloniaProperty targetProperty, - object anchor = null) + object anchor = null, + bool enableDataValidation = false) { Contract.Requires(target != null); var pathInfo = ParsePath(Path); ValidateState(pathInfo); + enableDataValidation = enableDataValidation && Priority == BindingPriority.LocalValue; ExpressionObserver observer; @@ -105,7 +104,7 @@ namespace Avalonia.Markup.Xaml.Data } else if (Source != null) { - observer = CreateSourceObserver(Source, pathInfo.Path); + observer = CreateSourceObserver(Source, pathInfo.Path, enableDataValidation); } else if (RelativeSource == null || RelativeSource.Mode == RelativeSourceMode.DataContext) { @@ -113,7 +112,8 @@ namespace Avalonia.Markup.Xaml.Data target, pathInfo.Path, targetProperty == Control.DataContextProperty, - anchor); + anchor, + enableDataValidation); } else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent) { @@ -135,7 +135,7 @@ namespace Avalonia.Markup.Xaml.Data fallback = null; } - var subject = new ExpressionSubject( + var subject = new BindingExpression( observer, targetProperty?.PropertyType ?? typeof(object), fallback, @@ -197,7 +197,8 @@ namespace Avalonia.Markup.Xaml.Data IAvaloniaObject target, string path, bool targetIsDataContext, - object anchor) + object anchor, + bool enableDataValidation) { Contract.Requires(target != null); @@ -220,19 +221,16 @@ namespace Avalonia.Markup.Xaml.Data () => target.GetValue(Control.DataContextProperty), path, update, - EnableValidation); + enableDataValidation); return result; } else { return new ExpressionObserver( - target.GetObservable(Visual.VisualParentProperty) - .OfType() - .Select(x => x.GetObservable(Control.DataContextProperty)) - .Switch(), + GetParentDataContext(target), path, - EnableValidation); + enableDataValidation); } } @@ -240,18 +238,23 @@ namespace Avalonia.Markup.Xaml.Data { Contract.Requires(target != null); + var description = $"#{elementName}.{path}"; var result = new ExpressionObserver( ControlLocator.Track(target, elementName), path, - EnableValidation); + false, + description); return result; } - private ExpressionObserver CreateSourceObserver(object source, string path) + private ExpressionObserver CreateSourceObserver( + object source, + string path, + bool enabledDataValidation) { Contract.Requires(source != null); - return new ExpressionObserver(source, path, EnableValidation); + return new ExpressionObserver(source, path, enabledDataValidation); } private ExpressionObserver CreateTemplatedParentObserver( @@ -272,6 +275,22 @@ namespace Avalonia.Markup.Xaml.Data return result; } + private IObservable GetParentDataContext(IAvaloniaObject target) + { + // The DataContext is based on the visual parent and not the logical parent: this may + // seem unintuitive considering the fact that property inheritance works on the logical + // tree, but consider a ContentControl with a ContentPresenter. The ContentControl's + // Content property is bound to a value which becomes the ContentPresenter's + // DataContext - it is from this that the child hosted by the ContentPresenter needs to + // inherit its DataContext. + return target.GetObservable(Visual.VisualParentProperty) + .Select(x => + { + return (x as IAvaloniaObject)?.GetObservable(Control.DataContextProperty) ?? + Observable.Return((object)null); + }).Switch(); + } + private class PathInfo { public string Path { get; set; } diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup.Xaml/Data/MultiBinding.cs index 91a502a44f..69190be220 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Data/MultiBinding.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Data/MultiBinding.cs @@ -53,7 +53,8 @@ namespace Avalonia.Markup.Xaml.Data public InstancedBinding Initiate( IAvaloniaObject target, AvaloniaProperty targetProperty, - object anchor = null) + object anchor = null, + bool enableDataValidation = false) { if (Converter == null) { diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/StyleResourceBinding.cs b/src/Markup/Avalonia.Markup.Xaml/Data/StyleResourceBinding.cs index 442f9199e8..c538c0768e 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Data/StyleResourceBinding.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Data/StyleResourceBinding.cs @@ -37,7 +37,8 @@ namespace Avalonia.Markup.Xaml.Data public InstancedBinding Initiate( IAvaloniaObject target, AvaloniaProperty targetProperty, - object anchor = null) + object anchor = null, + bool enableDataValidation = false) { if (Name == "Red") { diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs index 70d3f7d161..b131f07fd4 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs @@ -29,7 +29,6 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions Mode = Mode, Path = Path, Priority = Priority, - EnableValidation = EnableValidation, }; } @@ -41,6 +40,5 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions public string Path { get; set; } public BindingPriority Priority { get; set; } = BindingPriority.LocalValue; public object Source { get; set; } - public bool EnableValidation { get; set; } } } \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs index be4287605c..5e412633d2 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs @@ -5,14 +5,13 @@ using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Markup.Data; using System; +using System.Reactive.Linq; namespace Avalonia.Markup.Xaml.Templates { public class MemberSelector : IMemberSelector { - private ExpressionNode _expressionNode; private string _memberName; - private ExpressionNode _memberValueNode; public string MemberName { @@ -22,8 +21,6 @@ namespace Avalonia.Markup.Xaml.Templates if (_memberName != value) { _memberName = value; - _expressionNode = null; - _memberValueNode = null; } } } @@ -35,34 +32,11 @@ namespace Avalonia.Markup.Xaml.Templates return o; } - if (_expressionNode == null) - { - _expressionNode = ExpressionNodeBuilder.Build(MemberName); - - _memberValueNode = _expressionNode; - - while (_memberValueNode.Next != null) - { - _memberValueNode = _memberValueNode.Next; - } - } - - _expressionNode.Target = new WeakReference(o); - - object result = _memberValueNode.CurrentValue.Target; - - _expressionNode.Target = null; - - if (result == AvaloniaProperty.UnsetValue) - { - return null; - } - else if (result is BindingError) - { - return null; - } + var expression = new ExpressionObserver(o, MemberName); + object result = AvaloniaProperty.UnsetValue; - return result; + expression.Subscribe(x => result = x); + return (result == AvaloniaProperty.UnsetValue || result is BindingNotification) ? null : result; } } } \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj index 49a5a4a681..738f381f4e 100644 --- a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj +++ b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj @@ -41,14 +41,17 @@ Properties\SharedAssemblyInfo.cs + + - + + - + @@ -59,8 +62,12 @@ + + + + - + diff --git a/src/Markup/Avalonia.Markup/Data/BindingExpression.cs b/src/Markup/Avalonia.Markup/Data/BindingExpression.cs new file mode 100644 index 0000000000..0f4c091bff --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/BindingExpression.cs @@ -0,0 +1,305 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Globalization; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using Avalonia.Data; +using Avalonia.Logging; +using Avalonia.Utilities; + +namespace Avalonia.Markup.Data +{ + /// + /// Binds to an expression on an object using a type value converter to convert the values + /// that are send and received. + /// + public class BindingExpression : ISubject, IDescription + { + private readonly ExpressionObserver _inner; + private readonly Type _targetType; + private readonly object _fallbackValue; + private readonly BindingPriority _priority; + private readonly Subject _errors = new Subject(); + + /// + /// Initializes a new instance of the class. + /// + /// The . + /// The type to convert the value to. + public BindingExpression(ExpressionObserver inner, Type targetType) + : this(inner, targetType, DefaultValueConverter.Instance) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The . + /// The type to convert the value to. + /// The value converter to use. + /// + /// A parameter to pass to . + /// + /// The binding priority. + public BindingExpression( + ExpressionObserver inner, + Type targetType, + IValueConverter converter, + object converterParameter = null, + BindingPriority priority = BindingPriority.LocalValue) + : this(inner, targetType, AvaloniaProperty.UnsetValue, converter, converterParameter, priority) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The . + /// The type to convert the value to. + /// + /// The value to use when the binding is unable to produce a value. + /// + /// The value converter to use. + /// + /// A parameter to pass to . + /// + /// The binding priority. + public BindingExpression( + ExpressionObserver inner, + Type targetType, + object fallbackValue, + IValueConverter converter, + object converterParameter = null, + BindingPriority priority = BindingPriority.LocalValue) + { + Contract.Requires(inner != null); + Contract.Requires(targetType != null); + Contract.Requires(converter != null); + + _inner = inner; + _targetType = targetType; + Converter = converter; + ConverterParameter = converterParameter; + _fallbackValue = fallbackValue; + _priority = priority; + } + + /// + /// Gets the converter to use on the expression. + /// + public IValueConverter Converter { get; } + + /// + /// Gets a parameter to pass to . + /// + public object ConverterParameter { get; } + + /// + string IDescription.Description => _inner.Expression; + + /// + public void OnCompleted() + { + } + + /// + public void OnError(Exception error) + { + } + + /// + public void OnNext(object value) + { + using (_inner.Subscribe(_ => { })) + { + var type = _inner.ResultType; + + if (type != null) + { + var converted = Converter.ConvertBack( + value, + type, + ConverterParameter, + CultureInfo.CurrentUICulture); + + if (converted == AvaloniaProperty.UnsetValue) + { + converted = TypeUtilities.Default(type); + _inner.SetValue(converted, _priority); + } + else if (converted is BindingNotification) + { + var notification = converted as BindingNotification; + + if (notification.ErrorType == BindingErrorType.None) + { + throw new AvaloniaInternalException( + "IValueConverter should not return non-errored BindingNotification."); + } + + _errors.OnNext(notification); + + if (_fallbackValue != AvaloniaProperty.UnsetValue) + { + if (TypeUtilities.TryConvert( + type, + _fallbackValue, + CultureInfo.InvariantCulture, + out converted)) + { + _inner.SetValue(converted, _priority); + } + else + { + Logger.Error( + LogArea.Binding, + this, + "Could not convert FallbackValue {FallbackValue} to {Type}", + _fallbackValue, + type); + } + } + } + else + { + _inner.SetValue(converted, _priority); + } + } + } + } + + /// + public IDisposable Subscribe(IObserver observer) + { + return _inner.Select(ConvertValue).Merge(_errors).Subscribe(observer); + } + + private object ConvertValue(object value) + { + var notification = value as BindingNotification; + + if (notification == null) + { + var converted = Converter.Convert( + value, + _targetType, + ConverterParameter, + CultureInfo.CurrentUICulture); + + notification = converted as BindingNotification; + + if (notification?.ErrorType == BindingErrorType.None) + { + converted = notification.Value; + } + + if (_fallbackValue != AvaloniaProperty.UnsetValue && + (converted == AvaloniaProperty.UnsetValue || converted is BindingNotification)) + { + var fallback = ConvertFallback(); + converted = Merge(converted, fallback); + } + + return converted; + } + else + { + return ConvertValue(notification); + } + } + + private BindingNotification ConvertValue(BindingNotification notification) + { + if (notification.HasValue) + { + var converted = ConvertValue(notification.Value); + notification = Merge(notification, converted); + } + else if (_fallbackValue != AvaloniaProperty.UnsetValue) + { + var fallback = ConvertFallback(); + notification = Merge(notification, fallback); + } + + return notification; + } + + private BindingNotification ConvertFallback() + { + object converted; + + if (_fallbackValue == AvaloniaProperty.UnsetValue) + { + throw new AvaloniaInternalException("Cannot call ConvertFallback with no fallback value"); + } + + if (TypeUtilities.TryConvert( + _targetType, + _fallbackValue, + CultureInfo.InvariantCulture, + out converted)) + { + return new BindingNotification(converted); + } + else + { + return new BindingNotification( + new InvalidCastException( + $"Could not convert FallbackValue '{_fallbackValue}' to '{_targetType}'"), + BindingErrorType.Error); + } + } + + private static BindingNotification Merge(object a, BindingNotification b) + { + var an = a as BindingNotification; + + if (an != null) + { + Merge(an, b); + return an; + } + else + { + return b; + } + } + + private static BindingNotification Merge(BindingNotification a, object b) + { + var bn = b as BindingNotification; + + if (bn != null) + { + Merge(a, bn); + } + else + { + a.SetValue(b); + } + + return a; + } + + private static BindingNotification Merge(BindingNotification a, BindingNotification b) + { + if (b.HasValue) + { + a.SetValue(b.Value); + } + else + { + a.ClearValue(); + } + + if (b.Error != null) + { + a.AddError(b.Error, b.ErrorType); + } + + return a; + } + } +} diff --git a/src/Markup/Avalonia.Markup/Data/EmptyExpressionNode.cs b/src/Markup/Avalonia.Markup/Data/EmptyExpressionNode.cs new file mode 100644 index 0000000000..02ecd817da --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/EmptyExpressionNode.cs @@ -0,0 +1,18 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Reactive.Linq; + +namespace Avalonia.Markup.Data +{ + internal class EmptyExpressionNode : ExpressionNode + { + public override string Description => "."; + + protected override IObservable StartListeningCore(WeakReference reference) + { + return Observable.Return(reference.Target); + } + } +} diff --git a/src/Markup/Avalonia.Markup/Data/ExpressionNode.cs b/src/Markup/Avalonia.Markup/Data/ExpressionNode.cs index 90a654d9e6..b0957c7187 100644 --- a/src/Markup/Avalonia.Markup/Data/ExpressionNode.cs +++ b/src/Markup/Avalonia.Markup/Data/ExpressionNode.cs @@ -2,124 +2,188 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Reactive.Disposables; +using System.Reactive.Linq; using System.Reactive.Subjects; using Avalonia.Data; namespace Avalonia.Markup.Data { - internal abstract class ExpressionNode : IObservable + internal abstract class ExpressionNode : ISubject { protected static readonly WeakReference UnsetReference = new WeakReference(AvaloniaProperty.UnsetValue); - private WeakReference _target; - - private Subject _subject; - - private WeakReference _value = UnsetReference; + private WeakReference _target = UnsetReference; + private IDisposable _valueSubscription; + private IObserver _observer; + private IDisposable _valuePluginSubscription; + public abstract string Description { get; } public ExpressionNode Next { get; set; } public WeakReference Target { - get - { - return _target; - } + get { return _target; } set { - var newInstance = value?.Target; - var oldInstance = _target?.Target; + Contract.Requires(value != null); - if (!object.Equals(oldInstance, newInstance)) - { - if (oldInstance != null) - { - Unsubscribe(oldInstance); - } + var oldTarget = _target?.Target; + var newTarget = value.Target; + var running = _valueSubscription != null; + if (!ReferenceEquals(oldTarget, newTarget)) + { + _valueSubscription?.Dispose(); + _valueSubscription = null; + _valuePluginSubscription?.Dispose(); _target = value; - if (newInstance != null) + if (running) { - SubscribeAndUpdate(_target); - } - else - { - CurrentValue = UnsetReference; - } - - if (Next != null) - { - Next.Target = _value; + _valueSubscription = StartListening(); } } } } - public WeakReference CurrentValue + public IDisposable Subscribe(IObserver observer) { - get + if (_observer != null) { - return _value; + throw new AvaloniaInternalException("ExpressionNode can only be subscribed once."); } - set + _observer = observer; + var nextSubscription = Next?.Subscribe(this); + _valueSubscription = StartListening(); + + return Disposable.Create(() => { - _value = value; + _valueSubscription?.Dispose(); + _valueSubscription = null; + _valuePluginSubscription?.Dispose(); + _valuePluginSubscription = null; + nextSubscription?.Dispose(); + _observer = null; + }); + } - if (Next != null) - { - Next.Target = value; - } + void IObserver.OnCompleted() + { + throw new AvaloniaInternalException("ExpressionNode.OnCompleted should not be called."); + } - _subject?.OnNext(value.Target); - } + void IObserver.OnError(Exception error) + { + throw new AvaloniaInternalException("ExpressionNode.OnError should not be called."); } - public virtual bool SetValue(object value, BindingPriority priority) + void IObserver.OnNext(object value) { - return Next?.SetValue(value, priority) ?? false; + NextValueChanged(value); } - public virtual IDisposable Subscribe(IObserver observer) + protected virtual IObservable StartListeningCore(WeakReference reference) { - if (Next != null) + return Observable.Return(reference.Target); + } + + protected virtual void NextValueChanged(object value) + { + var bindingBroken = BindingNotification.ExtractError(value) as MarkupBindingChainNullException; + bindingBroken?.AddNode(Description); + _observer.OnNext(value); + } + + private IDisposable StartListening() + { + var target = _target.Target; + IObservable source; + + if (target == null) + { + source = Observable.Return(TargetNullNotification()); + } + else if (target == AvaloniaProperty.UnsetValue) { - return Next.Subscribe(observer); + source = Observable.Empty(); } else { - if (_subject == null) - { - _subject = new Subject(); - } - - observer.OnNext(CurrentValue.Target); - return _subject.Subscribe(observer); + source = StartListeningCore(_target); } - } - protected virtual void SubscribeAndUpdate(WeakReference reference) - { - CurrentValue = reference; + return source.Subscribe(TargetValueChanged); } - protected virtual void SendValidationStatus(IValidationStatus status) + private void TargetValueChanged(object value) { - //Even if elements only bound to sub-values, send validation changes along so they will be surfaced to the UI level. - if (_subject != null) + var notification = value as BindingNotification; + + if (notification == null) { - _subject.OnNext(status); + if (!HandleSpecialValue(value)) + { + if (Next != null) + { + Next.Target = new WeakReference(value); + } + else + { + _observer.OnNext(value); + } + } } else { - Next?.SendValidationStatus(status); + if (notification.Error != null) + { + _observer.OnNext(notification); + } + else if (notification.HasValue) + { + if (!HandleSpecialValue(notification.Value)) + { + if (Next != null) + { + Next.Target = new WeakReference(notification.Value); + } + else + { + _observer.OnNext(value); + } + } + } + } + } + + private bool HandleSpecialValue(object value) + { + if (_valuePluginSubscription == null) + { + var reference = new WeakReference(value); + + foreach (var plugin in ExpressionObserver.ValueHandlers) + { + if (plugin.Match(reference)) + { + _valuePluginSubscription = plugin.Start(reference)?.Subscribe(TargetValueChanged); + return true; + } + } } + + return false; } - protected virtual void Unsubscribe(object target) + private BindingNotification TargetNullNotification() { + return new BindingNotification( + new MarkupBindingChainNullException(), + BindingErrorType.Error, + AvaloniaProperty.UnsetValue); } } } diff --git a/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs b/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs index 0b5fbc6d7d..819949b7b9 100644 --- a/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs +++ b/src/Markup/Avalonia.Markup/Data/ExpressionObserver.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; +using System.Reactive.Subjects; using Avalonia.Data; using Avalonia.Markup.Data.Plugins; @@ -31,41 +32,57 @@ namespace Avalonia.Markup.Data /// An ordered collection of validation checker plugins that can be used to customize /// the validation of view model and model data. /// - public static readonly IList ValidationCheckers = - new List + public static readonly IList DataValidators = + new List { + new DataAnnotationsValidationPlugin(), new IndeiValidationPlugin(), + new ExceptionValidationPlugin(), }; - private readonly WeakReference _root; - private readonly Func _rootGetter; - private readonly IObservable _rootObservable; - private readonly IObservable _update; - private IDisposable _rootObserverSubscription; - private IDisposable _updateSubscription; - private int _count; + /// + /// An ordered collection of value handlers that can be used to customize the handling + /// of certain values. + /// + public static readonly IList ValueHandlers = + new List + { + new TaskValuePlugin(), + new ObservableValuePlugin(), + }; + + private static readonly object UninitializedValue = new object(); private readonly ExpressionNode _node; - private bool _enableValidation; + private readonly Subject _finished; + private readonly object _root; + private IObservable _result; /// /// Initializes a new instance of the class. /// /// The root object. /// The expression. - /// Whether property validation should be enabled. - public ExpressionObserver(object root, string expression, bool enableValidation = false) + /// Whether data validation should be enabled. + /// + /// A description of the expression. If null, will be used. + /// + public ExpressionObserver( + object root, + string expression, + bool enableDataValidation = false, + string description = null) { Contract.Requires(expression != null); - _root = new WeakReference(root); - _enableValidation = enableValidation; - - if (!string.IsNullOrWhiteSpace(expression)) + if (root == AvaloniaProperty.UnsetValue) { - _node = ExpressionNodeBuilder.Build(expression, enableValidation); + root = null; } Expression = expression; + Description = description ?? expression; + _node = Parse(expression, enableDataValidation); + _root = new WeakReference(root); } /// @@ -73,24 +90,24 @@ namespace Avalonia.Markup.Data /// /// An observable which provides the root object. /// The expression. - /// Whether property validation should be enabled. + /// Whether data validation should be enabled. + /// + /// A description of the expression. If null, will be used. + /// public ExpressionObserver( IObservable rootObservable, string expression, - bool enableValidation = false) + bool enableDataValidation = false, + string description = null) { Contract.Requires(rootObservable != null); Contract.Requires(expression != null); - _rootObservable = rootObservable; - _enableValidation = enableValidation; - - if (!string.IsNullOrWhiteSpace(expression)) - { - _node = ExpressionNodeBuilder.Build(expression, enableValidation); - } - Expression = expression; + Description = description ?? expression; + _node = Parse(expression, enableDataValidation); + _finished = new Subject(); + _root = rootObservable; } /// @@ -99,27 +116,28 @@ namespace Avalonia.Markup.Data /// A function which gets the root object. /// The expression. /// An observable which triggers a re-read of the getter. - /// Whether property validation should be enabled. + /// Whether data validation should be enabled. + /// + /// A description of the expression. If null, will be used. + /// public ExpressionObserver( Func rootGetter, string expression, IObservable update, - bool enableValidation = false) + bool enableDataValidation = false, + string description = null) { Contract.Requires(rootGetter != null); Contract.Requires(expression != null); Contract.Requires(update != null); - _rootGetter = rootGetter; - _update = update; - _enableValidation = enableValidation; - - if (!string.IsNullOrWhiteSpace(expression)) - { - _node = ExpressionNodeBuilder.Build(expression, enableValidation); - } - Expression = expression; + Description = description ?? expression; + _node = Parse(expression, enableDataValidation); + _finished = new Subject(); + + _node.Target = new WeakReference(rootGetter()); + _root = update.Select(x => rootGetter()); } /// @@ -129,27 +147,20 @@ namespace Avalonia.Markup.Data /// The binding priority to use. /// /// True if the value could be set; false if the expression does not evaluate to a - /// property. + /// property. Note that the must be subscribed to + /// before setting the target value can work, as setting the value requires the + /// expression to be evaluated. /// public bool SetValue(object value, BindingPriority priority = BindingPriority.LocalValue) { - IncrementCount(); - - if (_rootGetter != null && _node != null) - { - _node.Target = new WeakReference(_rootGetter()); - } - - try - { - return _node?.SetValue(value, priority) ?? false; - } - finally - { - DecrementCount(); - } + return (Leaf as PropertyAccessorNode)?.SetTargetValue(value, priority) ?? false; } + /// + /// Gets a description of the expression being observed. + /// + public string Description { get; } + /// /// Gets the expression being observed. /// @@ -159,41 +170,7 @@ namespace Avalonia.Markup.Data /// Gets the type of the expression result or null if the expression could not be /// evaluated. /// - public Type ResultType - { - get - { - IncrementCount(); - - try - { - if (_node != null) - { - return (Leaf as PropertyAccessorNode)?.PropertyType; - } - else if (_rootGetter != null) - { - return _rootGetter()?.GetType(); - } - else - { - return _root.Target?.GetType(); - } - } - finally - { - DecrementCount(); - } - } - } - - /// - string IDescription.Description => Expression; - - /// - /// Gets the root expression node. Used for testing. - /// - internal ExpressionNode Node => _node; + public Type ResultType => (Leaf as PropertyAccessorNode)?.PropertyType; /// /// Gets the leaf node. @@ -211,94 +188,88 @@ namespace Avalonia.Markup.Data /// protected override IDisposable SubscribeCore(IObserver observer) { - IncrementCount(); - - if (_node != null) + if (_result == null) { - IObservable source = _node; + var source = (IObservable)_node; - if (_rootObservable != null) - { - source = source.TakeUntil(_rootObservable.LastOrDefaultAsync()); - } - else if (_update != null) + if (_finished != null) { - source = source.TakeUntil(_update.LastOrDefaultAsync()); + source = source.TakeUntil(_finished); } - var subscription = source.Subscribe(observer); - - return Disposable.Create(() => - { - DecrementCount(); - subscription.Dispose(); - }); + _result = Observable.Using(StartRoot, _ => source) + .Select(ToWeakReference) + .Publish(UninitializedValue) + .RefCount() + .Where(x => x != UninitializedValue) + .Select(Translate); } - else if (_rootObservable != null) + + return _result.Subscribe(observer); + } + + private static ExpressionNode Parse(string expression, bool enableDataValidation) + { + if (!string.IsNullOrWhiteSpace(expression)) { - return _rootObservable.Subscribe(observer); + return ExpressionNodeBuilder.Build(expression, enableDataValidation); } else { - if (_update == null) - { - return Observable.Never() - .StartWith(_root.Target) - .Subscribe(observer); - } - else - { - return _update - .Select(_ => _rootGetter()) - .StartWith(_rootGetter()) - .Subscribe(observer); - } + return new EmptyExpressionNode(); } } - private void IncrementCount() + private static object ToWeakReference(object o) + { + return o is BindingNotification ? o : new WeakReference(o); + } + + private object Translate(object o) { - if (_count++ == 0 && _node != null) + var weak = o as WeakReference; + + if (weak != null) { - if (_rootGetter != null) - { - _node.Target = new WeakReference(_rootGetter()); + return weak.Target; + } + else + { + var broken = BindingNotification.ExtractError(o) as MarkupBindingChainNullException; - if (_update != null) + if (broken != null) + { + // We've received notification of a broken expression due to a null value + // somewhere in the chain. If this null value occurs at the first node then we + // ignore it, as its likely that e.g. the DataContext has not yet been set up. + if (broken.HasNodes) { - _updateSubscription = _update.Subscribe(x => - _node.Target = new WeakReference(_rootGetter())); + broken.Commit(Description); + } + else + { + o = AvaloniaProperty.UnsetValue; } } - else if (_rootObservable != null) - { - _rootObserverSubscription = _rootObservable.Subscribe(x => - _node.Target = new WeakReference(x)); - } - else - { - _node.Target = _root; - } + return o; } } - private void DecrementCount() + private IDisposable StartRoot() { - if (--_count == 0 && _node != null) - { - if (_rootObserverSubscription != null) - { - _rootObserverSubscription.Dispose(); - _rootObserverSubscription = null; - } + var observable = _root as IObservable; - if (_updateSubscription != null) - { - _updateSubscription.Dispose(); - _updateSubscription = null; - } - - _node.Target = null; + if (observable != null) + { + return observable.Subscribe( + x => _node.Target = new WeakReference(x != AvaloniaProperty.UnsetValue ? x : null), + _ => _finished.OnNext(Unit.Default), + () => _finished.OnNext(Unit.Default)); + } + else + { + _node.Target = (WeakReference)_root; + return Disposable.Empty; } } } diff --git a/src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs b/src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs deleted file mode 100644 index 0a3be26c18..0000000000 --- a/src/Markup/Avalonia.Markup/Data/ExpressionSubject.cs +++ /dev/null @@ -1,213 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using System.Globalization; -using System.Reactive.Linq; -using System.Reactive.Subjects; -using Avalonia.Data; -using Avalonia.Logging; -using Avalonia.Utilities; - -namespace Avalonia.Markup.Data -{ - /// - /// Turns an into a subject that can be bound two-way with - /// a value converter. - /// - public class ExpressionSubject : ISubject, IDescription - { - private readonly ExpressionObserver _inner; - private readonly Type _targetType; - private readonly object _fallbackValue; - private readonly BindingPriority _priority; - - /// - /// Initializes a new instance of the class. - /// - /// The . - /// The type to convert the value to. - public ExpressionSubject(ExpressionObserver inner, Type targetType) - : this(inner, targetType, DefaultValueConverter.Instance) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The . - /// The type to convert the value to. - /// The value converter to use. - /// - /// A parameter to pass to . - /// - /// The binding priority. - public ExpressionSubject( - ExpressionObserver inner, - Type targetType, - IValueConverter converter, - object converterParameter = null, - BindingPriority priority = BindingPriority.LocalValue) - : this(inner, targetType, AvaloniaProperty.UnsetValue, converter, converterParameter, priority) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The . - /// The type to convert the value to. - /// - /// The value to use when the binding is unable to produce a value. - /// - /// The value converter to use. - /// - /// A parameter to pass to . - /// - /// The binding priority. - public ExpressionSubject( - ExpressionObserver inner, - Type targetType, - object fallbackValue, - IValueConverter converter, - object converterParameter = null, - BindingPriority priority = BindingPriority.LocalValue) - { - Contract.Requires(inner != null); - Contract.Requires(targetType != null); - Contract.Requires(converter != null); - - _inner = inner; - _targetType = targetType; - Converter = converter; - ConverterParameter = converterParameter; - _fallbackValue = fallbackValue; - _priority = priority; - } - - /// - /// Gets the converter to use on the expression. - /// - public IValueConverter Converter { get; } - - /// - /// Gets a parameter to pass to . - /// - public object ConverterParameter { get; } - - /// - string IDescription.Description => _inner.Expression; - - /// - public void OnCompleted() - { - } - - /// - public void OnError(Exception error) - { - } - - /// - public void OnNext(object value) - { - var type = _inner.ResultType; - - if (type != null) - { - var converted = Converter.ConvertBack( - value, - type, - ConverterParameter, - CultureInfo.CurrentUICulture); - - if (converted == AvaloniaProperty.UnsetValue) - { - converted = TypeUtilities.Default(type); - _inner.SetValue(converted, _priority); - } - else if (converted is BindingError) - { - var error = converted as BindingError; - - Logger.Error( - LogArea.Binding, - this, - "Error binding to {Expression}: {Message}", - _inner.Expression, - error.Exception.Message); - - if (_fallbackValue != AvaloniaProperty.UnsetValue) - { - if (TypeUtilities.TryConvert( - type, - _fallbackValue, - CultureInfo.InvariantCulture, - out converted)) - { - _inner.SetValue(converted, _priority); - } - else - { - Logger.Error( - LogArea.Binding, - this, - "Could not convert FallbackValue {FallbackValue} to {Type}", - _fallbackValue, - type); - } - } - } - else - { - _inner.SetValue(converted, _priority); - } - } - } - - /// - public IDisposable Subscribe(IObserver observer) - { - return _inner.Select(ConvertValue).Subscribe(observer); - } - - private object ConvertValue(object value) - { - var converted = - value as BindingError ?? - value as IValidationStatus ?? - Converter.Convert( - value, - _targetType, - ConverterParameter, - CultureInfo.CurrentUICulture); - - if (_fallbackValue != AvaloniaProperty.UnsetValue && - (converted == AvaloniaProperty.UnsetValue || - converted is BindingError)) - { - var error = converted as BindingError; - - if (TypeUtilities.TryConvert( - _targetType, - _fallbackValue, - CultureInfo.InvariantCulture, - out converted)) - { - if (error != null) - { - converted = new BindingError(error.Exception, converted); - } - } - else - { - converted = new BindingError( - new InvalidCastException( - $"Could not convert FallbackValue '{_fallbackValue}' to '{_targetType}'")); - } - } - - return converted; - } - } -} diff --git a/src/Markup/Avalonia.Markup/Data/IndexerNode.cs b/src/Markup/Avalonia.Markup/Data/IndexerNode.cs index 8849e7edbc..0a6b93bad1 100644 --- a/src/Markup/Avalonia.Markup/Data/IndexerNode.cs +++ b/src/Markup/Avalonia.Markup/Data/IndexerNode.cs @@ -10,130 +10,49 @@ using System.ComponentModel; using System.Globalization; using System.Linq; using System.Reflection; +using System.Reactive.Linq; namespace Avalonia.Markup.Data { - internal class IndexerNode : ExpressionNode, - IWeakSubscriber, - IWeakSubscriber + internal class IndexerNode : ExpressionNode { public IndexerNode(IList arguments) { Arguments = arguments; } - public IList Arguments { get; } + public override string Description => "[" + string.Join(",", Arguments) + "]"; - void IWeakSubscriber.OnEvent(object sender, NotifyCollectionChangedEventArgs e) + protected override IObservable StartListeningCore(WeakReference reference) { - var update = false; - if (sender is IList) - { - object indexObject; - if (!TypeUtilities.TryConvert(typeof(int), Arguments[0], CultureInfo.InvariantCulture, out indexObject)) - { - return; - } - var index = (int)indexObject; - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - update = index >= e.NewStartingIndex; - break; - case NotifyCollectionChangedAction.Remove: - update = index >= e.OldStartingIndex; - break; - case NotifyCollectionChangedAction.Replace: - update = index >= e.NewStartingIndex && - index < e.NewStartingIndex + e.NewItems.Count; - break; - case NotifyCollectionChangedAction.Move: - update = (index >= e.NewStartingIndex && - index < e.NewStartingIndex + e.NewItems.Count) || - (index >= e.OldStartingIndex && - index < e.OldStartingIndex + e.OldItems.Count); - break; - case NotifyCollectionChangedAction.Reset: - update = true; - break; - } - } - else - { - update = true; - } - - if (update) - { - CurrentValue = new WeakReference(GetValue(sender)); - } - } - - void IWeakSubscriber.OnEvent(object sender, PropertyChangedEventArgs e) - { - var typeInfo = sender.GetType().GetTypeInfo(); - - if (typeInfo.GetDeclaredProperty(e.PropertyName) == null) - { - return; - } - - if (typeInfo.GetDeclaredProperty(e.PropertyName).GetIndexParameters().Any()) - { - CurrentValue = new WeakReference(GetValue(sender)); - } - } - - protected override void SubscribeAndUpdate(WeakReference reference) - { - object target = reference.Target; - - CurrentValue = new WeakReference(GetValue(target)); - + var target = reference.Target; var incc = target as INotifyCollectionChanged; - - if (incc != null) - { - WeakSubscriptionManager.Subscribe( - incc, - nameof(incc.CollectionChanged), - this); - } - var inpc = target as INotifyPropertyChanged; - - if (inpc != null) - { - WeakSubscriptionManager.Subscribe( - inpc, - nameof(inpc.PropertyChanged), - this); - } - } - - protected override void Unsubscribe(object target) - { - var incc = target as INotifyCollectionChanged; + var inputs = new List>(); if (incc != null) { - WeakSubscriptionManager.Unsubscribe( - incc, - nameof(incc.CollectionChanged), - this); + inputs.Add(WeakObservable.FromEventPattern( + target, + nameof(incc.CollectionChanged)) + .Where(x => ShouldUpdate(x.Sender, x.EventArgs)) + .Select(_ => GetValue(target))); } - var inpc = target as INotifyPropertyChanged; - if (inpc != null) { - WeakSubscriptionManager.Unsubscribe( - inpc, - nameof(inpc.PropertyChanged), - this); + inputs.Add(WeakObservable.FromEventPattern( + target, + nameof(inpc.PropertyChanged)) + .Where(x => ShouldUpdate(x.Sender, x.EventArgs)) + .Select(_ => GetValue(target))); } + + return Observable.Merge(inputs).StartWith(GetValue(target)); } + public IList Arguments { get; } + private object GetValue(object target) { var typeInfo = target.GetType().GetTypeInfo(); @@ -141,18 +60,23 @@ namespace Avalonia.Markup.Data var dictionary = target as IDictionary; var indexerProperty = GetIndexer(typeInfo); var indexerParameters = indexerProperty?.GetIndexParameters(); + if (indexerProperty != null && indexerParameters.Length == Arguments.Count) { var convertedObjectArray = new object[indexerParameters.Length]; + for (int i = 0; i < Arguments.Count; i++) { object temp = null; + if (!TypeUtilities.TryConvert(indexerParameters[i].ParameterType, Arguments[i], CultureInfo.InvariantCulture, out temp)) { return AvaloniaProperty.UnsetValue; } + convertedObjectArray[i] = temp; } + var intArgs = convertedObjectArray.OfType().ToArray(); // Try special cases where we can validate indicies @@ -166,16 +90,18 @@ namespace Avalonia.Markup.Data { if (intArgs.Length == Arguments.Count && intArgs[0] >= 0 && intArgs[0] < list.Count) { - return list[intArgs[0]]; + return list[intArgs[0]]; } + return AvaloniaProperty.UnsetValue; } else if (dictionary != null) { if (dictionary.Contains(convertedObjectArray[0])) { - return dictionary[convertedObjectArray[0]]; + return dictionary[convertedObjectArray[0]]; } + return AvaloniaProperty.UnsetValue; } else @@ -187,11 +113,11 @@ namespace Avalonia.Markup.Data else { // Fallback to unchecked access - return indexerProperty.GetValue(target, convertedObjectArray); + return indexerProperty.GetValue(target, convertedObjectArray); } } // Multidimensional arrays end up here because the indexer search picks up the IList indexer instead of the - // multidimensional indexer, which doesn't take the same number of arguments + // multidimensional indexer, which doesn't take the same number of arguments else if (typeInfo.IsArray) { return GetValueFromArray((Array)target); @@ -220,13 +146,16 @@ namespace Avalonia.Markup.Data private bool ConvertArgumentsToInts(out int[] intArgs) { intArgs = new int[Arguments.Count]; + for (int i = 0; i < Arguments.Count; ++i) { object value; + if (!TypeUtilities.TryConvert(typeof(int), Arguments[i], CultureInfo.InvariantCulture, out value)) { return false; } + intArgs[i] = (int)value; } return true; @@ -235,7 +164,8 @@ namespace Avalonia.Markup.Data private static PropertyInfo GetIndexer(TypeInfo typeInfo) { PropertyInfo indexer; - for (;typeInfo != null; typeInfo = typeInfo.BaseType?.GetTypeInfo()) + + for (; typeInfo != null; typeInfo = typeInfo.BaseType?.GetTypeInfo()) { // Check for the default indexer name first to make this faster. // This will only be false when a class in VB has a custom indexer name. @@ -243,14 +173,16 @@ namespace Avalonia.Markup.Data { return indexer; } + foreach (var property in typeInfo.DeclaredProperties) { if (property.GetIndexParameters().Any()) { return property; } - } + } } + return null; } @@ -273,5 +205,46 @@ namespace Avalonia.Markup.Data return false; } } + + private bool ShouldUpdate(object sender, NotifyCollectionChangedEventArgs e) + { + if (sender is IList) + { + object indexObject; + + if (!TypeUtilities.TryConvert(typeof(int), Arguments[0], CultureInfo.InvariantCulture, out indexObject)) + { + return false; + } + + var index = (int)indexObject; + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + return index >= e.NewStartingIndex; + case NotifyCollectionChangedAction.Remove: + return index >= e.OldStartingIndex; + case NotifyCollectionChangedAction.Replace: + return index >= e.NewStartingIndex && + index < e.NewStartingIndex + e.NewItems.Count; + case NotifyCollectionChangedAction.Move: + return (index >= e.NewStartingIndex && + index < e.NewStartingIndex + e.NewItems.Count) || + (index >= e.OldStartingIndex && + index < e.OldStartingIndex + e.OldItems.Count); + case NotifyCollectionChangedAction.Reset: + return true; + } + } + + return false; + } + + private bool ShouldUpdate(object sender, PropertyChangedEventArgs e) + { + var typeInfo = sender.GetType().GetTypeInfo(); + return typeInfo.GetDeclaredProperty(e.PropertyName)?.GetIndexParameters().Any() ?? false; + } } } diff --git a/src/Markup/Avalonia.Markup/Data/LogicalNotNode.cs b/src/Markup/Avalonia.Markup/Data/LogicalNotNode.cs index 29fa842439..58a7915254 100644 --- a/src/Markup/Avalonia.Markup/Data/LogicalNotNode.cs +++ b/src/Markup/Avalonia.Markup/Data/LogicalNotNode.cs @@ -3,21 +3,17 @@ using System; using System.Globalization; -using System.Reactive.Linq; using Avalonia.Data; namespace Avalonia.Markup.Data { internal class LogicalNotNode : ExpressionNode { - public override bool SetValue(object value, BindingPriority priority) - { - return false; - } + public override string Description => "!"; - public override IDisposable Subscribe(IObserver observer) + protected override void NextValueChanged(object value) { - return Next.Select(Negate).Subscribe(observer); + base.NextValueChanged(Negate(value)); } private static object Negate(object v) @@ -34,6 +30,12 @@ namespace Avalonia.Markup.Data { return !result; } + else + { + return new BindingNotification( + new InvalidCastException($"Unable to convert '{s}' to bool."), + BindingErrorType.Error); + } } else { @@ -42,9 +44,17 @@ namespace Avalonia.Markup.Data var boolean = Convert.ToBoolean(v, CultureInfo.InvariantCulture); return !boolean; } - catch + catch (InvalidCastException) + { + // The error message here is "Unable to cast object of type 'System.Object' + // to type 'System.IConvertible'" which is kinda useless so provide our own. + return new BindingNotification( + new InvalidCastException($"Unable to convert '{v}' to bool."), + BindingErrorType.Error); + } + catch (Exception e) { - // TODO: Maybe should log something here. + return new BindingNotification(e, BindingErrorType.Error); } } } diff --git a/src/Markup/Avalonia.Markup/Data/MarkupBindingChainNullException.cs b/src/Markup/Avalonia.Markup/Data/MarkupBindingChainNullException.cs new file mode 100644 index 0000000000..a549d6ebb6 --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/MarkupBindingChainNullException.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Linq; +using Avalonia.Data; + +namespace Avalonia.Markup.Data +{ + internal class MarkupBindingChainNullException : BindingChainNullException + { + private IList _nodes = new List(); + + public MarkupBindingChainNullException() + { + } + + public MarkupBindingChainNullException(string expression, string expressionNullPoint) + : base(expression, expressionNullPoint) + { + _nodes = null; + } + + public bool HasNodes => _nodes.Count > 0; + public void AddNode(string node) => _nodes.Add(node); + + public void Commit(string expression) + { + Expression = expression; + ExpressionNullPoint = string.Join(".", _nodes.Reverse()) + .Replace(".!", "!") + .Replace(".[", "["); + _nodes = null; + } + } +} diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/AvaloniaPropertyAccessorPlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/AvaloniaPropertyAccessorPlugin.cs index b2d73ab8fc..1d4a9a688e 100644 --- a/src/Markup/Avalonia.Markup/Data/Plugins/AvaloniaPropertyAccessorPlugin.cs +++ b/src/Markup/Avalonia.Markup/Data/Plugins/AvaloniaPropertyAccessorPlugin.cs @@ -4,7 +4,6 @@ using System; using System.Reactive.Linq; using Avalonia.Data; -using Avalonia.Logging; namespace Avalonia.Markup.Data.Plugins { @@ -13,36 +12,22 @@ namespace Avalonia.Markup.Data.Plugins /// public class AvaloniaPropertyAccessorPlugin : IPropertyAccessorPlugin { - /// - /// Checks whether this plugin can handle accessing the properties of the specified object. - /// - /// A weak reference to the object. - /// True if the plugin can handle the object; otherwise false. - public bool Match(WeakReference reference) - { - Contract.Requires(reference != null); - - return reference.Target is AvaloniaObject; - } + /// + public bool Match(WeakReference reference) => reference.Target is AvaloniaObject; /// /// Starts monitoring the value of a property on an object. /// /// A weak reference to the object. /// The property name. - /// A function to call when the property changes. /// /// An interface through which future interactions with the /// property will be made. /// - public IPropertyAccessor Start( - WeakReference reference, - string propertyName, - Action changed) + public IPropertyAccessor Start(WeakReference reference, string propertyName) { Contract.Requires(reference != null); Contract.Requires(propertyName != null); - Contract.Requires(changed != null); var instance = reference.Target; var o = (AvaloniaObject)instance; @@ -50,13 +35,13 @@ namespace Avalonia.Markup.Data.Plugins if (p != null) { - return new Accessor(new WeakReference(o), p, changed); + return new Accessor(new WeakReference(o), p); } else if (instance != AvaloniaProperty.UnsetValue) { var message = $"Could not find AvaloniaProperty '{propertyName}' on '{instance}'"; var exception = new MissingMemberException(message); - return new PropertyError(new BindingError(exception)); + return new PropertyError(new BindingNotification(exception, BindingErrorType.Error)); } else { @@ -64,23 +49,19 @@ namespace Avalonia.Markup.Data.Plugins } } - private class Accessor : IPropertyAccessor + private class Accessor : PropertyAccessorBase { private readonly WeakReference _reference; private readonly AvaloniaProperty _property; private IDisposable _subscription; - public Accessor( - WeakReference reference, - AvaloniaProperty property, - Action changed) + public Accessor(WeakReference reference, AvaloniaProperty property) { Contract.Requires(reference != null); Contract.Requires(property != null); _reference = reference; _property = property; - _subscription = Instance.GetWeakObservable(property).Skip(1).Subscribe(changed); } public AvaloniaObject Instance @@ -93,17 +74,10 @@ namespace Avalonia.Markup.Data.Plugins } } - public Type PropertyType => _property.PropertyType; - - public object Value => Instance.GetValue(_property); + public override Type PropertyType => _property.PropertyType; + public override object Value => Instance?.GetValue(_property); - public void Dispose() - { - _subscription?.Dispose(); - _subscription = null; - } - - public bool SetValue(object value, BindingPriority priority) + public override bool SetValue(object value, BindingPriority priority) { if (!_property.IsReadOnly) { @@ -113,6 +87,17 @@ namespace Avalonia.Markup.Data.Plugins return false; } + + protected override void Dispose(bool disposing) + { + _subscription?.Dispose(); + _subscription = null; + } + + protected override void SubscribeCore(IObserver observer) + { + _subscription = Instance.GetWeakObservable(_property).Subscribe(observer); + } } } } diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/DataAnnotationsValidationPlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/DataAnnotationsValidationPlugin.cs new file mode 100644 index 0000000000..859438636a --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/Plugins/DataAnnotationsValidationPlugin.cs @@ -0,0 +1,81 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Reflection; +using Avalonia.Data; + +namespace Avalonia.Markup.Data.Plugins +{ + /// + /// Validates properties on that have s. + /// + public class DataAnnotationsValidationPlugin : IDataValidationPlugin + { + /// + public bool Match(WeakReference reference, string memberName) + { + return reference.Target? + .GetType() + .GetRuntimeProperty(memberName)? + .GetCustomAttributes() + .Any() ?? false; + } + + /// + public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor inner) + { + return new Accessor(reference, name, inner); + } + + private class Accessor : DataValidatiorBase + { + private ValidationContext _context; + + public Accessor(WeakReference reference, string name, IPropertyAccessor inner) + : base(inner) + { + _context = new ValidationContext(reference.Target); + _context.MemberName = name; + } + + public override bool SetValue(object value, BindingPriority priority) + { + return base.SetValue(value, priority); + } + + protected override void InnerValueChanged(object value) + { + var errors = new List(); + + if (Validator.TryValidateProperty(value, _context, errors)) + { + base.InnerValueChanged(value); + } + else + { + base.InnerValueChanged(new BindingNotification( + CreateException(errors), + BindingErrorType.DataValidationError, + value)); + } + } + + private Exception CreateException(IList errors) + { + if (errors.Count == 1) + { + return new ValidationException(errors[0].ErrorMessage); + } + else + { + return new AggregateException( + errors.Select(x => new ValidationException(x.ErrorMessage))); + } + } + } + } +} diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/DataValidatiorBase.cs b/src/Markup/Avalonia.Markup/Data/Plugins/DataValidatiorBase.cs new file mode 100644 index 0000000000..95d269f437 --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/Plugins/DataValidatiorBase.cs @@ -0,0 +1,80 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Data; + +namespace Avalonia.Markup.Data.Plugins +{ + /// + /// Base class for data validators. + /// + /// + /// Data validators are s that are returned from an + /// . They wrap an inner + /// and convert any values received from the inner property accessor into + /// s. + /// + public abstract class DataValidatiorBase : PropertyAccessorBase, IObserver + { + private readonly IPropertyAccessor _inner; + + /// + /// Initializes a new instance of the class. + /// + /// The inner property accessor. + protected DataValidatiorBase(IPropertyAccessor inner) + { + _inner = inner; + } + + /// + public override Type PropertyType => _inner.PropertyType; + + /// + public override object Value => _inner.Value; + + /// + public override bool SetValue(object value, BindingPriority priority) => _inner.SetValue(value, priority); + + /// + /// Should never be called: the inner should never notify + /// completion. + /// + void IObserver.OnCompleted() { } + + /// + /// Should never be called: the inner should never notify + /// an error. + /// + void IObserver.OnError(Exception error) { } + + /// + /// Called when the inner notifies with a new value. + /// + /// The value. + void IObserver.OnNext(object value) => InnerValueChanged(value); + + /// + protected override void Dispose(bool disposing) => _inner.Dispose(); + + /// + /// Begins listening to the inner . + /// + protected override void SubscribeCore(IObserver observer) => _inner.Subscribe(this); + + /// + /// Called when the inner notifies with a new value. + /// + /// The value. + /// + /// Notifies the observer that the value has changed. The value will be wrapped in a + /// if it is not already a binding notification. + /// + protected virtual void InnerValueChanged(object value) + { + var notification = value as BindingNotification ?? new BindingNotification(value); + Observer.OnNext(notification); + } + } +} \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/ExceptionValidationPlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/ExceptionValidationPlugin.cs index 95a32a6928..e0b6bcfd7c 100644 --- a/src/Markup/Avalonia.Markup/Data/Plugins/ExceptionValidationPlugin.cs +++ b/src/Markup/Avalonia.Markup/Data/Plugins/ExceptionValidationPlugin.cs @@ -10,23 +10,21 @@ namespace Avalonia.Markup.Data.Plugins /// /// Validates properties that report errors by throwing exceptions. /// - public class ExceptionValidationPlugin : IValidationPlugin + public class ExceptionValidationPlugin : IDataValidationPlugin { - public static ExceptionValidationPlugin Instance { get; } = new ExceptionValidationPlugin(); - /// - public bool Match(WeakReference reference) => true; + public bool Match(WeakReference reference, string memberName) => true; /// - public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor, Action callback) + public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor inner) { - return new ExceptionValidationChecker(reference, name, accessor, callback); + return new Validator(reference, name, inner); } - private class ExceptionValidationChecker : ValidatingPropertyAccessorBase + private class Validator : DataValidatiorBase { - public ExceptionValidationChecker(WeakReference reference, string name, IPropertyAccessor accessor, Action callback) - : base(reference, name, accessor, callback) + public Validator(WeakReference reference, string name, IPropertyAccessor inner) + : base(inner) { } @@ -34,39 +32,19 @@ namespace Avalonia.Markup.Data.Plugins { try { - var success = base.SetValue(value, priority); - SendValidationCallback(new ExceptionValidationStatus(null)); - return success; + return base.SetValue(value, priority); } catch (TargetInvocationException ex) { - SendValidationCallback(new ExceptionValidationStatus(ex.InnerException)); + Observer.OnNext(new BindingNotification(ex.InnerException, BindingErrorType.DataValidationError)); } catch (Exception ex) { - SendValidationCallback(new ExceptionValidationStatus(ex)); + Observer.OnNext(new BindingNotification(ex, BindingErrorType.DataValidationError)); } - return false; - } - } - /// - /// Describes the current validation status after setting a property value. - /// - public class ExceptionValidationStatus : IValidationStatus - { - internal ExceptionValidationStatus(Exception exception) - { - Exception = exception; + return false; } - - /// - /// The thrown exception. If there was no thrown exception, null. - /// - public Exception Exception { get; } - - /// - public bool IsValid => Exception == null; } } } diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/IDataValidationPlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/IDataValidationPlugin.cs new file mode 100644 index 0000000000..0952e2edab --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/Plugins/IDataValidationPlugin.cs @@ -0,0 +1,37 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Data; + +namespace Avalonia.Markup.Data.Plugins +{ + /// + /// Defines how data validation is observed by an . + /// + public interface IDataValidationPlugin + { + /// + /// Checks whether this plugin can handle data validation on the specified object. + /// + /// A weak reference to the object. + /// The name of the member to validate. + /// True if the plugin can handle the object; otherwise false. + bool Match(WeakReference reference, string memberName); + + /// + /// Starts monitoring the data validation state of a property on an object. + /// + /// A weak reference to the object. + /// The property name. + /// The inner property accessor used to aceess the property. + /// + /// An interface through which future interactions with the + /// property will be made. + /// + IPropertyAccessor Start( + WeakReference reference, + string propertyName, + IPropertyAccessor inner); + } +} diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessor.cs b/src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessor.cs index d9da4b906f..9e686baf10 100644 --- a/src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessor.cs +++ b/src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessor.cs @@ -10,11 +10,14 @@ namespace Avalonia.Markup.Data.Plugins /// Defines an accessor to a property on an object returned by a /// /// - public interface IPropertyAccessor : IDisposable + public interface IPropertyAccessor : IObservable, IDisposable { /// /// Gets the type of the property. /// + /// + /// The accessor has not been subscribed to yet. + /// Type PropertyType { get; } /// diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessorPlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessorPlugin.cs index 0d6b57e424..187142bd0e 100644 --- a/src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessorPlugin.cs +++ b/src/Markup/Avalonia.Markup/Data/Plugins/IPropertyAccessorPlugin.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections; namespace Avalonia.Markup.Data.Plugins { @@ -24,14 +23,12 @@ namespace Avalonia.Markup.Data.Plugins /// /// A weak reference to the object. /// The property name. - /// A function to call when the property changes. /// /// An interface through which future interactions with the /// property will be made. /// IPropertyAccessor Start( WeakReference reference, - string propertyName, - Action changed); + string propertyName); } } diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/IValidationPlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/IValidationPlugin.cs deleted file mode 100644 index e42e1ad7b2..0000000000 --- a/src/Markup/Avalonia.Markup/Data/Plugins/IValidationPlugin.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) The Avalonia Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using Avalonia.Data; - -namespace Avalonia.Markup.Data.Plugins -{ - /// - /// Defines how view model data validation is observed by an . - /// - public interface IValidationPlugin - { - - /// - /// Checks whether the data uses a validation scheme supported by this plugin. - /// - /// A weak reference to the data. - /// true if this plugin can observe the validation; otherwise, false. - bool Match(WeakReference reference); - - /// - /// Starts monitoring the validation state of an object for the given property. - /// - /// A weak reference to the object. - /// The property name. - /// An underlying to access the property. - /// A function to call when the validation state changes. - /// - /// A subclass through which future interactions with the - /// property will be made. - /// - IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor, Action callback); - } -} diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/IValuePlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/IValuePlugin.cs new file mode 100644 index 0000000000..fb285c6d73 --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/Plugins/IValuePlugin.cs @@ -0,0 +1,29 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; + +namespace Avalonia.Markup.Data.Plugins +{ + /// + /// Defines how values are observed by an . + /// + public interface IValuePlugin + { + /// + /// Checks whether this plugin handles the specified value. + /// + /// A weak reference to the value. + /// True if the plugin can handle the value; otherwise false. + bool Match(WeakReference reference); + + /// + /// Starts producing output based on the specified value. + /// + /// A weak reference to the object. + /// + /// An observable that produces the output for the value. + /// + IObservable Start(WeakReference reference); + } +} diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/IndeiValidationPlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/IndeiValidationPlugin.cs index f6b4aea614..82bc87c207 100644 --- a/src/Markup/Avalonia.Markup/Data/Plugins/IndeiValidationPlugin.cs +++ b/src/Markup/Avalonia.Markup/Data/Plugins/IndeiValidationPlugin.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections; +using System.Collections.Generic; using System.ComponentModel; using System.Linq; using Avalonia.Data; @@ -13,79 +13,106 @@ namespace Avalonia.Markup.Data.Plugins /// /// Validates properties on objects that implement . /// - public class IndeiValidationPlugin : IValidationPlugin + public class IndeiValidationPlugin : IDataValidationPlugin { /// - public bool Match(WeakReference reference) - { - return reference.Target is INotifyDataErrorInfo; - } + public bool Match(WeakReference reference, string memberName) => reference.Target is INotifyDataErrorInfo; /// - public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor, Action callback) + public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor) { - return new IndeiValidationChecker(reference, name, accessor, callback); + return new Validator(reference, name, accessor); } - private class IndeiValidationChecker : ValidatingPropertyAccessorBase, IWeakSubscriber + private class Validator : DataValidatiorBase, IWeakSubscriber { - public IndeiValidationChecker(WeakReference reference, string name, IPropertyAccessor accessor, Action callback) - : base(reference, name, accessor, callback) + WeakReference _reference; + string _name; + + public Validator(WeakReference reference, string name, IPropertyAccessor inner) + : base(inner) + { + _reference = reference; + _name = name; + } + + void IWeakSubscriber.OnEvent(object sender, DataErrorsChangedEventArgs e) { - var target = reference.Target as INotifyDataErrorInfo; + if (e.PropertyName == _name || string.IsNullOrEmpty(e.PropertyName)) + { + Observer.OnNext(CreateBindingNotification(Value)); + } + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + var target = _reference.Target as INotifyDataErrorInfo; + if (target != null) { - if (target.HasErrors) - { - SendValidationCallback(new IndeiValidationStatus(target.GetErrors(name))); - } - WeakSubscriptionManager.Subscribe( + WeakSubscriptionManager.Unsubscribe( target, nameof(target.ErrorsChanged), this); } } - public override void Dispose() + protected override void SubscribeCore(IObserver observer) { - base.Dispose(); var target = _reference.Target as INotifyDataErrorInfo; + if (target != null) { - WeakSubscriptionManager.Unsubscribe( + WeakSubscriptionManager.Subscribe( target, nameof(target.ErrorsChanged), this); } + + base.SubscribeCore(observer); } - public void OnEvent(object sender, DataErrorsChangedEventArgs e) + protected override void InnerValueChanged(object value) { - if (e.PropertyName == _name || string.IsNullOrEmpty(e.PropertyName)) + base.InnerValueChanged(CreateBindingNotification(value)); + } + + private BindingNotification CreateBindingNotification(object value) + { + var target = (INotifyDataErrorInfo)_reference.Target; + + if (target != null) { - var indei = _reference.Target as INotifyDataErrorInfo; - SendValidationCallback(new IndeiValidationStatus(indei.GetErrors(e.PropertyName))); + var errors = target.GetErrors(_name)? + .Cast() + .Where(x => x != null).ToList(); + + if (errors?.Count > 0) + { + return new BindingNotification( + GenerateException(errors), + BindingErrorType.DataValidationError, + value); + } } + + return new BindingNotification(value); } - } - /// - /// Describes the current validation status of a property as reported by an object that implements . - /// - public class IndeiValidationStatus : IValidationStatus - { - internal IndeiValidationStatus(IEnumerable errors) + private Exception GenerateException(IList errors) { - Errors = errors; + if (errors.Count == 1) + { + return new Exception(errors[0]); + } + else + { + return new AggregateException( + errors.Select(x => new Exception(x))); + } } - - /// - public bool IsValid => !Errors?.OfType().Any() ?? true; - - /// - /// The errors on the given property and on the object as a whole. - /// - public IEnumerable Errors { get; } } } } diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs index 36046dd742..138f09b373 100644 --- a/src/Markup/Avalonia.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Markup/Avalonia.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs @@ -9,7 +9,6 @@ using System.Reflection; using Avalonia.Data; using Avalonia.Logging; using Avalonia.Utilities; -using System.Collections; namespace Avalonia.Markup.Data.Plugins { @@ -19,124 +18,130 @@ namespace Avalonia.Markup.Data.Plugins /// public class InpcPropertyAccessorPlugin : IPropertyAccessorPlugin { - /// - /// Checks whether this plugin can handle accessing the properties of the specified object. - /// - /// The object. - /// True if the plugin can handle the object; otherwise false. - public bool Match(WeakReference reference) - { - Contract.Requires(reference != null); - - return true; - } + /// + public bool Match(WeakReference reference) => true; /// /// Starts monitoring the value of a property on an object. /// /// The object. /// The property name. - /// A function to call when the property changes. /// /// An interface through which future interactions with the /// property will be made. /// - public IPropertyAccessor Start( - WeakReference reference, - string propertyName, - Action changed) + public IPropertyAccessor Start(WeakReference reference, string propertyName) { Contract.Requires(reference != null); Contract.Requires(propertyName != null); - Contract.Requires(changed != null); var instance = reference.Target; var p = instance.GetType().GetRuntimeProperties().FirstOrDefault(_ => _.Name == propertyName); if (p != null) { - return new Accessor(reference, p, changed); + return new Accessor(reference, p); } else { var message = $"Could not find CLR property '{propertyName}' on '{instance}'"; var exception = new MissingMemberException(message); - return new PropertyError(new BindingError(exception)); + return new PropertyError(new BindingNotification(exception, BindingErrorType.Error)); } } - private class Accessor : IPropertyAccessor, IWeakSubscriber + private class Accessor : PropertyAccessorBase, IWeakSubscriber { private readonly WeakReference _reference; private readonly PropertyInfo _property; - private readonly Action _changed; + private bool _eventRaised; - public Accessor( - WeakReference reference, - PropertyInfo property, - Action changed) + public Accessor(WeakReference reference, PropertyInfo property) { Contract.Requires(reference != null); Contract.Requires(property != null); _reference = reference; _property = property; - _changed = changed; + } - var inpc = reference.Target as INotifyPropertyChanged; + public override Type PropertyType => _property.PropertyType; - if (inpc != null) + public override object Value + { + get { - WeakSubscriptionManager.Subscribe( - inpc, - nameof(inpc.PropertyChanged), - this); + var o = _reference.Target; + return (o != null) ? _property.GetValue(o) : null; } - else + } + + public override bool SetValue(object value, BindingPriority priority) + { + if (_property.CanWrite) { - Logger.Warning( - LogArea.Binding, - this, - "Bound to property {Property} on {Source} which does not implement INotifyPropertyChanged", - property.Name, - reference.Target, - reference.Target.GetType()); + _eventRaised = false; + _property.SetValue(_reference.Target, value); + + if (!_eventRaised) + { + SendCurrentValue(); + } + + return true; } - } - public Type PropertyType => _property.PropertyType; + return false; + } - public object Value => _property.GetValue(_reference.Target); + void IWeakSubscriber.OnEvent(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName)) + { + _eventRaised = true; + SendCurrentValue(); + } + } - public void Dispose() + protected override void Dispose(bool disposing) { var inpc = _reference.Target as INotifyPropertyChanged; if (inpc != null) { - WeakSubscriptionManager.Unsubscribe( + WeakSubscriptionManager.Unsubscribe( inpc, nameof(inpc.PropertyChanged), this); } } - public bool SetValue(object value, BindingPriority priority) + protected override void SubscribeCore(IObserver observer) { - if (_property.CanWrite) + SendCurrentValue(); + SubscribeToChanges(); + } + + private void SendCurrentValue() + { + try { - _property.SetValue(_reference.Target, value); - return true; + var value = Value; + Observer.OnNext(value); } - - return false; + catch { } } - void IWeakSubscriber.OnEvent(object sender, PropertyChangedEventArgs e) + private void SubscribeToChanges() { - if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName)) + var inpc = _reference.Target as INotifyPropertyChanged; + + if (inpc != null) { - _changed(Value); + WeakSubscriptionManager.Subscribe( + inpc, + nameof(inpc.PropertyChanged), + this); } } } diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/ObservableValuePlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/ObservableValuePlugin.cs new file mode 100644 index 0000000000..a406fc55b9 --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/Plugins/ObservableValuePlugin.cs @@ -0,0 +1,44 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Reflection; +using System.Threading.Tasks; +using System.Windows.Input; +using Avalonia.Data; + +namespace Avalonia.Markup.Data.Plugins +{ + /// + /// Handles binding to s in an . + /// + public class ObservableValuePlugin : IValuePlugin + { + /// + /// Checks whether this plugin handles the specified value. + /// + /// A weak reference to the value. + /// True if the plugin can handle the value; otherwise false. + public virtual bool Match(WeakReference reference) + { + var target = reference.Target; + + // ReactiveCommand is an IObservable but we want to bind to it, not its value. + return target is IObservable && !(target is ICommand); + } + + /// + /// Starts producing output based on the specified value. + /// + /// A weak reference to the object. + /// + /// An observable that produces the output for the value. + /// + public virtual IObservable Start(WeakReference reference) + { + return reference.Target as IObservable; + } + } +} diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/PropertyAccessorBase.cs b/src/Markup/Avalonia.Markup/Data/Plugins/PropertyAccessorBase.cs new file mode 100644 index 0000000000..9aa858e0eb --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/Plugins/PropertyAccessorBase.cs @@ -0,0 +1,68 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Data; + +namespace Avalonia.Markup.Data.Plugins +{ + /// + /// Defines a default base implementation for a . + /// + /// + /// is an observable that will only be subscribed to one time. + /// In addition, the subscription can be disposed by calling on the + /// property accessor itself - this prevents needing to hold two references for a subscription. + /// + public abstract class PropertyAccessorBase : IPropertyAccessor + { + /// + public abstract Type PropertyType { get; } + + /// + public abstract object Value { get; } + + /// + /// Stops the subscription. + /// + public void Dispose() => Dispose(true); + + /// + public abstract bool SetValue(object value, BindingPriority priority); + + /// + /// The currently subscribed observer. + /// + protected IObserver Observer { get; private set; } + + /// + public IDisposable Subscribe(IObserver observer) + { + Contract.Requires(observer != null); + + if (Observer != null) + { + throw new InvalidOperationException( + "A property accessor can be subscribed to only once."); + } + + Observer = observer; + SubscribeCore(observer); + return this; + } + + /// + /// Stops listening to the property. + /// + /// + /// True if the method was called, false if the object is being + /// finalized. + /// + protected virtual void Dispose(bool disposing) => Observer = null; + + /// + /// When overridden in a derived class, begins listening to the property. + /// + protected abstract void SubscribeCore(IObserver observer); + } +} diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/PropertyError.cs b/src/Markup/Avalonia.Markup/Data/Plugins/PropertyError.cs index f73bb1fc94..b351ef39bd 100644 --- a/src/Markup/Avalonia.Markup/Data/Plugins/PropertyError.cs +++ b/src/Markup/Avalonia.Markup/Data/Plugins/PropertyError.cs @@ -1,4 +1,5 @@ using System; +using System.Reactive.Disposables; using Avalonia.Data; namespace Avalonia.Markup.Data.Plugins @@ -8,13 +9,13 @@ namespace Avalonia.Markup.Data.Plugins /// public class PropertyError : IPropertyAccessor { - private BindingError _error; + private BindingNotification _error; /// /// Initializes a new instance of the class. /// /// The error to report. - public PropertyError(BindingError error) + public PropertyError(BindingNotification error) { _error = error; } @@ -35,5 +36,11 @@ namespace Avalonia.Markup.Data.Plugins { return false; } + + public IDisposable Subscribe(IObserver observer) + { + observer.OnNext(_error); + return Disposable.Empty; + } } } diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/TaskValuePlugin.cs b/src/Markup/Avalonia.Markup/Data/Plugins/TaskValuePlugin.cs new file mode 100644 index 0000000000..b6fda67503 --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/Plugins/TaskValuePlugin.cs @@ -0,0 +1,82 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Reactive.Concurrency; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Reflection; +using System.Threading.Tasks; +using Avalonia.Data; + +namespace Avalonia.Markup.Data.Plugins +{ + /// + /// Handles binding to s in an . + /// + public class TaskValuePlugin : IValuePlugin + { + /// + /// Checks whether this plugin handles the specified value. + /// + /// A weak reference to the value. + /// True if the plugin can handle the value; otherwise false. + public virtual bool Match(WeakReference reference) => reference.Target is Task; + + /// + /// Starts producing output based on the specified value. + /// + /// A weak reference to the object. + /// + /// An observable that produces the output for the value. + /// + public virtual IObservable Start(WeakReference reference) + { + var task = reference.Target as Task; + + if (task != null) + { + var resultProperty = task.GetType().GetTypeInfo().GetDeclaredProperty("Result"); + + if (resultProperty != null) + { + switch (task.Status) + { + case TaskStatus.RanToCompletion: + case TaskStatus.Faulted: + return HandleCompleted(task); + default: + var subject = new Subject(); + task.ContinueWith( + x => HandleCompleted(task).Subscribe(subject), + TaskScheduler.FromCurrentSynchronizationContext()) + .ConfigureAwait(false); + return subject; + } + } + } + + return Observable.Empty(); + } + + protected IObservable HandleCompleted(Task task) + { + var resultProperty = task.GetType().GetTypeInfo().GetDeclaredProperty("Result"); + + if (resultProperty != null) + { + switch (task.Status) + { + case TaskStatus.RanToCompletion: + return Observable.Return(resultProperty.GetValue(task)); + case TaskStatus.Faulted: + return Observable.Return(new BindingNotification(task.Exception, BindingErrorType.Error)); + default: + throw new AvaloniaInternalException("HandleCompleted called for non-completed Task."); + } + } + + return Observable.Empty(); + } + } +} diff --git a/src/Markup/Avalonia.Markup/Data/Plugins/ValidatingPropertyAccessorBase.cs b/src/Markup/Avalonia.Markup/Data/Plugins/ValidatingPropertyAccessorBase.cs deleted file mode 100644 index 07ac600a8a..0000000000 --- a/src/Markup/Avalonia.Markup/Data/Plugins/ValidatingPropertyAccessorBase.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using Avalonia.Data; - -namespace Avalonia.Markup.Data.Plugins -{ - - /// - /// A base class for validating s that wraps an and forwards method calls to it. - /// - public abstract class ValidatingPropertyAccessorBase : IPropertyAccessor - { - protected readonly WeakReference _reference; - protected readonly string _name; - private readonly IPropertyAccessor _accessor; - private readonly Action _callback; - - protected ValidatingPropertyAccessorBase(WeakReference reference, string name, IPropertyAccessor accessor, Action callback) - { - _reference = reference; - _name = name; - _accessor = accessor; - _callback = callback; - } - - /// - public Type PropertyType => _accessor.PropertyType; - - /// - public object Value => _accessor.Value; - - /// - public virtual void Dispose() => _accessor.Dispose(); - - /// - public virtual bool SetValue(object value, BindingPriority priority) => _accessor.SetValue(value, priority); - - /// - /// Sends the validation status to the callback specified in construction. - /// - /// The validation status. - protected void SendValidationCallback(IValidationStatus status) - { - _callback?.Invoke(status); - } - } -} \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup/Data/PropertyAccessorNode.cs b/src/Markup/Avalonia.Markup/Data/PropertyAccessorNode.cs index b12fbe90eb..7fb3137417 100644 --- a/src/Markup/Avalonia.Markup/Data/PropertyAccessorNode.cs +++ b/src/Markup/Avalonia.Markup/Data/PropertyAccessorNode.cs @@ -3,11 +3,8 @@ using System; using System.Linq; +using System.Reactive.Disposables; using System.Reactive.Linq; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using System.Windows.Input; using Avalonia.Data; using Avalonia.Markup.Data.Plugins; @@ -15,9 +12,8 @@ namespace Avalonia.Markup.Data { internal class PropertyAccessorNode : ExpressionNode { + private readonly bool _enableValidation; private IPropertyAccessor _accessor; - private IDisposable _subscription; - private bool _enableValidation; public PropertyAccessorNode(string propertyName, bool enableValidation) { @@ -25,118 +21,44 @@ namespace Avalonia.Markup.Data _enableValidation = enableValidation; } + public override string Description => PropertyName; public string PropertyName { get; } - public Type PropertyType => _accessor?.PropertyType; - public override bool SetValue(object value, BindingPriority priority) + public bool SetTargetValue(object value, BindingPriority priority) { - if (Next != null) + if (_accessor != null) { - return Next.SetValue(value, priority); + try { return _accessor.SetValue(value, priority); } catch { } } - else - { - if (_accessor != null) - { - return _accessor.SetValue(value, priority); - } - return false; - } + return false; } - protected override void SubscribeAndUpdate(WeakReference reference) + protected override IObservable StartListeningCore(WeakReference reference) { - var instance = reference.Target; + var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference)); + var accessor = plugin?.Start(reference, PropertyName); - if (instance != null && instance != AvaloniaProperty.UnsetValue) + if (_enableValidation && Next == null) { - var accessorPlugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference)); - - if (accessorPlugin != null) + foreach (var validator in ExpressionObserver.DataValidators) { - _accessor = ExceptionValidationPlugin.Instance.Start( - reference, - PropertyName, - accessorPlugin.Start(reference, PropertyName, SetCurrentValue), - SendValidationStatus); - - if (_enableValidation) - { - foreach (var validationPlugin in ExpressionObserver.ValidationCheckers) - { - if (validationPlugin.Match(reference)) - { - _accessor = validationPlugin.Start(reference, PropertyName, _accessor, SendValidationStatus); - } - } - } - - if (_accessor != null) + if (validator.Match(reference, PropertyName)) { - SetCurrentValue(_accessor.Value); - return; + accessor = validator.Start(reference, PropertyName, accessor); } } } - CurrentValue = UnsetReference; - } - - protected override void Unsubscribe(object target) - { - _accessor?.Dispose(); - _accessor = null; - } - - private void SetCurrentValue(object value) - { - var observable = value as IObservable; - var command = value as ICommand; - var task = value as Task; - bool set = false; - - // HACK: ReactiveCommand is an IObservable but we want to bind to it, not its value. - // We may need to make this a more general solution. - if (observable != null && command == null) - { - CurrentValue = UnsetReference; - set = true; - _subscription = observable - .ObserveOn(SynchronizationContext.Current) - .Subscribe(x => CurrentValue = new WeakReference(x)); - } - else if (task != null) - { - var resultProperty = task.GetType().GetTypeInfo().GetDeclaredProperty("Result"); - - if (resultProperty != null) + // Ensure that _accessor is set for the duration of the subscription. + return Observable.Using( + () => { - if (task.Status == TaskStatus.RanToCompletion) - { - CurrentValue = new WeakReference(resultProperty.GetValue(task)); - set = true; - } - else - { - task.ContinueWith( - x => CurrentValue = new WeakReference(resultProperty.GetValue(task)), - TaskScheduler.FromCurrentSynchronizationContext()) - .ConfigureAwait(false); - } - } - } - else - { - CurrentValue = new WeakReference(value); - set = true; - } - - if (!set) - { - CurrentValue = UnsetReference; - } + _accessor = accessor; + return Disposable.Create(() => _accessor = null); + }, + _ => accessor); } } } diff --git a/src/Markup/Avalonia.Markup/DefaultValueConverter.cs b/src/Markup/Avalonia.Markup/DefaultValueConverter.cs index 469b00d3ad..86d37d8e13 100644 --- a/src/Markup/Avalonia.Markup/DefaultValueConverter.cs +++ b/src/Markup/Avalonia.Markup/DefaultValueConverter.cs @@ -43,8 +43,18 @@ namespace Avalonia.Markup if (value != null) { - var message = $"Could not convert '{value}' to '{targetType}'"; - return new BindingError(new InvalidCastException(message)); + string message; + + if (TypeUtilities.IsNumeric(targetType)) + { + message = $"'{value}' is not a valid number."; + } + else + { + message = $"Could not convert '{value}' to '{targetType.Name}'."; + } + + return new BindingNotification(new InvalidCastException(message), BindingErrorType.Error); } return AvaloniaProperty.UnsetValue; diff --git a/src/Markup/Avalonia.Markup/IValueConverter.cs b/src/Markup/Avalonia.Markup/IValueConverter.cs index 23117a3fac..10d5c626c2 100644 --- a/src/Markup/Avalonia.Markup/IValueConverter.cs +++ b/src/Markup/Avalonia.Markup/IValueConverter.cs @@ -3,6 +3,7 @@ using System; using System.Globalization; +using Avalonia.Data; namespace Avalonia.Markup { @@ -21,8 +22,8 @@ namespace Avalonia.Markup /// The converted value. /// /// This method should not throw exceptions. If the value is not convertible, return - /// . Any exception thrown will be treated as - /// an application exception. + /// a in an error state. Any exceptions thrown will be + /// treated as an application exception. /// object Convert(object value, Type targetType, object parameter, CultureInfo culture); @@ -36,8 +37,8 @@ namespace Avalonia.Markup /// The converted value. /// /// This method should not throw exceptions. If the value is not convertible, return - /// . Any exception thrown will be treated as - /// an application exception. + /// a in an error state. Any exceptions thrown will be + /// treated as an application exception. /// object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture); } diff --git a/src/Markup/Avalonia.Markup/Properties/AssemblyInfo.cs b/src/Markup/Avalonia.Markup/Properties/AssemblyInfo.cs index d74ccf00a1..dd8c0a6bd3 100644 --- a/src/Markup/Avalonia.Markup/Properties/AssemblyInfo.cs +++ b/src/Markup/Avalonia.Markup/Properties/AssemblyInfo.cs @@ -8,4 +8,3 @@ using System.Runtime.CompilerServices; [assembly: AssemblyTitle("Avalonia.Markup")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Markup")] [assembly: InternalsVisibleTo("Avalonia.Markup.UnitTests")] -[assembly: InternalsVisibleTo("Avalonia.Markup.Xaml")] \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup/packages.config b/src/Markup/Avalonia.Markup/packages.config index 9f732f1bcb..bcef21429a 100644 --- a/src/Markup/Avalonia.Markup/packages.config +++ b/src/Markup/Avalonia.Markup/packages.config @@ -1,5 +1,6 @@  + diff --git a/src/Skia/Avalonia.Skia.Android/Avalonia.Skia.Android.csproj b/src/Skia/Avalonia.Skia.Android/Avalonia.Skia.Android.csproj index a40ee92fbf..4a4b280771 100644 --- a/src/Skia/Avalonia.Skia.Android/Avalonia.Skia.Android.csproj +++ b/src/Skia/Avalonia.Skia.Android/Avalonia.Skia.Android.csproj @@ -39,8 +39,8 @@ - - ..\..\..\packages\SkiaSharp.1.53.0\lib\MonoAndroid\SkiaSharp.dll + + ..\..\..\packages\SkiaSharp.1.54.0\lib\MonoAndroid\SkiaSharp.dll True diff --git a/src/Skia/Avalonia.Skia.Android/packages.config b/src/Skia/Avalonia.Skia.Android/packages.config index 5f27ea1039..616c52f1d5 100644 --- a/src/Skia/Avalonia.Skia.Android/packages.config +++ b/src/Skia/Avalonia.Skia.Android/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia.Desktop/Avalonia.Skia.Desktop.csproj b/src/Skia/Avalonia.Skia.Desktop/Avalonia.Skia.Desktop.csproj index 8326df9881..95063bafce 100644 --- a/src/Skia/Avalonia.Skia.Desktop/Avalonia.Skia.Desktop.csproj +++ b/src/Skia/Avalonia.Skia.Desktop/Avalonia.Skia.Desktop.csproj @@ -56,8 +56,8 @@ MinimumRecommendedRules.ruleset - - ..\..\..\packages\SkiaSharp.1.53.0\lib\net45\SkiaSharp.dll + + ..\..\..\packages\SkiaSharp.1.54.0\lib\net45\SkiaSharp.dll True @@ -116,12 +116,12 @@ - + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - +