diff --git a/.gitmodules b/.gitmodules index 93467688c1..057007f213 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,4 +7,4 @@ branch = perspex-pcl [submodule "src/Markup/Perspex.Markup.Xaml/OmniXAML"] path = src/Markup/Perspex.Markup.Xaml/OmniXAML - url = https://github.com/SuperJMN/OmniXAML.git + url = https://github.com/Perspex/OmniXAML.git diff --git a/Perspex.sln b/Perspex.sln index 33c2badced..e2d114da8a 100644 --- a/Perspex.sln +++ b/Perspex.sln @@ -97,6 +97,12 @@ EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "PlatformSupport", "src\Shared\PlatformSupport\PlatformSupport.shproj", "{E4D9629C-F168-4224-3F51-A5E482FFBC42}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.TinyWM", "src\Perspex.TinyWM\Perspex.TinyWM.csproj", "{1D36D6B3-2994-41C5-8330-19FBDEC5769B}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Markup", "src\Markup\Perspex.Markup\Perspex.Markup.csproj", "{6417E941-21BC-467B-A771-0DE389353CE6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Markup.UnitTests", "tests\Perspex.Markup.UnitTests\Perspex.Markup.UnitTests.csproj", "{8EF392D5-1416-45AA-9956-7CBBC3229E8A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BindingTest", "samples\BindingTest\BindingTest.csproj", "{08B3E6B9-1CD5-443C-9F61-6D49D1C5F162}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XamlTestApplicationPcl", "samples\XamlTestApplicationPcl\XamlTestApplicationPcl.csproj", "{EA113F1A-D8D7-4142-9948-353270E7EBAE}" EndProject Global @@ -242,6 +248,18 @@ Global {1D36D6B3-2994-41C5-8330-19FBDEC5769B}.Debug|Any CPU.Build.0 = Debug|Any CPU {1D36D6B3-2994-41C5-8330-19FBDEC5769B}.Release|Any CPU.ActiveCfg = Release|Any CPU {1D36D6B3-2994-41C5-8330-19FBDEC5769B}.Release|Any CPU.Build.0 = Release|Any CPU + {6417E941-21BC-467B-A771-0DE389353CE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6417E941-21BC-467B-A771-0DE389353CE6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6417E941-21BC-467B-A771-0DE389353CE6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6417E941-21BC-467B-A771-0DE389353CE6}.Release|Any CPU.Build.0 = Release|Any CPU + {8EF392D5-1416-45AA-9956-7CBBC3229E8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8EF392D5-1416-45AA-9956-7CBBC3229E8A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8EF392D5-1416-45AA-9956-7CBBC3229E8A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8EF392D5-1416-45AA-9956-7CBBC3229E8A}.Release|Any CPU.Build.0 = Release|Any CPU + {08B3E6B9-1CD5-443C-9F61-6D49D1C5F162}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {08B3E6B9-1CD5-443C-9F61-6D49D1C5F162}.Debug|Any CPU.Build.0 = Debug|Any CPU + {08B3E6B9-1CD5-443C-9F61-6D49D1C5F162}.Release|Any CPU.ActiveCfg = Release|Any CPU + {08B3E6B9-1CD5-443C-9F61-6D49D1C5F162}.Release|Any CPU.Build.0 = Release|Any CPU {EA113F1A-D8D7-4142-9948-353270E7EBAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EA113F1A-D8D7-4142-9948-353270E7EBAE}.Debug|Any CPU.Build.0 = Debug|Any CPU {EA113F1A-D8D7-4142-9948-353270E7EBAE}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -271,6 +289,9 @@ Global {54F237D5-A70A-4752-9656-0C70B1A7B047} = {B9894058-278A-46B5-B6ED-AD613FCC03B3} {FB05AC90-89BA-4F2F-A924-F37875FB547C} = {B9894058-278A-46B5-B6ED-AD613FCC03B3} {E4D9629C-F168-4224-3F51-A5E482FFBC42} = {A689DEF5-D50F-4975-8B72-124C9EB54066} + {6417E941-21BC-467B-A771-0DE389353CE6} = {8B6A8209-894F-4BA1-B880-965FD453982C} + {8EF392D5-1416-45AA-9956-7CBBC3229E8A} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {08B3E6B9-1CD5-443C-9F61-6D49D1C5F162} = {9B9E3891-2366-4253-A952-D08BCEB71098} {EA113F1A-D8D7-4142-9948-353270E7EBAE} = {9B9E3891-2366-4253-A952-D08BCEB71098} EndGlobalSection EndGlobal diff --git a/nuget/build-version.ps1 b/nuget/build-version.ps1 index 1cce68564d..f458b30ffd 100644 --- a/nuget/build-version.ps1 +++ b/nuget/build-version.ps1 @@ -28,6 +28,8 @@ Copy-Item ..\src\Perspex.Styling\bin\Release\Perspex.Styling.dll $lib Copy-Item ..\src\Perspex.Styling\bin\Release\Perspex.Styling.xml $lib Copy-Item ..\src\Perspex.Themes.Default\bin\Release\Perspex.Themes.Default.dll $lib Copy-Item ..\src\Perspex.Themes.Default\bin\Release\Perspex.Themes.Default.xml $lib +Copy-Item ..\src\Markup\Perspex.Markup\bin\Release\Perspex.Markup.dll $lib +Copy-Item ..\src\Markup\Perspex.Markup\bin\Release\Perspex.Markup.xml $lib Copy-Item ..\src\Markup\Perspex.Markup.Xaml\bin\Release\Perspex.Markup.Xaml.dll $lib Copy-Item ..\src\Markup\Perspex.Markup.Xaml\bin\Release\Perspex.Markup.Xaml.xml $lib Copy-Item ..\src\Perspex.HtmlRenderer\bin\Release\Perspex.HtmlRenderer.dll $lib diff --git a/samples/BindingTest/App.config b/samples/BindingTest/App.config new file mode 100644 index 0000000000..8324aa6ff1 --- /dev/null +++ b/samples/BindingTest/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/samples/BindingTest/App.cs b/samples/BindingTest/App.cs new file mode 100644 index 0000000000..70cfcfb44b --- /dev/null +++ b/samples/BindingTest/App.cs @@ -0,0 +1,40 @@ +using System; +using Perspex; +using Perspex.Controls; +using Perspex.Diagnostics; +using Perspex.Themes.Default; +using Serilog; +using Serilog.Filters; + +namespace BindingTest +{ + public class App : Application + { + public App() + { + RegisterServices(); + InitializeSubsystems((int)Environment.OSVersion.Platform); + Styles = new DefaultTheme(); + + Log.Logger = new LoggerConfiguration() + .Filter.ByIncludingOnly(Matching.WithProperty("Area", "Property")) + .Filter.ByIncludingOnly(Matching.WithProperty("Property", "Text")) + .MinimumLevel.Verbose() + .WriteTo.Trace(outputTemplate: "[{Id:X8}] [{SourceContext}] {Message}") + .CreateLogger(); + } + + public static void AttachDevTools(Window window) + { + DevTools.Attach(window); + } + + private static void Main() + { + var app = new App(); + var window = new MainWindow(); + window.Show(); + app.Run(window); + } + } +} diff --git a/samples/BindingTest/BindingTest.csproj b/samples/BindingTest/BindingTest.csproj new file mode 100644 index 0000000000..62914fdc13 --- /dev/null +++ b/samples/BindingTest/BindingTest.csproj @@ -0,0 +1,167 @@ + + + + + Debug + AnyCPU + {08B3E6B9-1CD5-443C-9F61-6D49D1C5F162} + WinExe + Properties + BindingTest + BindingTest + v4.6 + 512 + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + ..\..\packages\Serilog.1.5.9\lib\net45\Serilog.dll + True + + + ..\..\packages\Serilog.1.5.9\lib\net45\Serilog.FullNetFx.dll + True + + + ..\..\packages\Splat.1.6.2\lib\Net45\Splat.dll + True + + + + + ..\..\packages\Rx-Core.2.2.5\lib\net45\System.Reactive.Core.dll + True + + + ..\..\packages\Rx-Interfaces.2.2.5\lib\net45\System.Reactive.Interfaces.dll + True + + + ..\..\packages\Rx-Linq.2.2.5\lib\net45\System.Reactive.Linq.dll + True + + + ..\..\packages\Rx-PlatformServices.2.2.5\lib\net45\System.Reactive.PlatformServices.dll + True + + + + + + + + + + + + MainWindow.paml + + + + + + + + + Designer + MSBuild:Compile + + + + + + {3e53a01a-b331-47f3-b828-4a5717e77a24} + Perspex.Markup.Xaml + + + {6417e941-21bc-467b-a771-0de389353ce6} + Perspex.Markup + + + {d211e587-d8bc-45b9-95a4-f297c8fa5200} + Perspex.Animation + + + {799a7bb5-3c2c-48b6-85a7-406a12c420da} + Perspex.Application + + + {b09b78d8-9b26-48b0-9149-d64a2f120f3f} + Perspex.Base + + + {d2221c82-4a25-4583-9b43-d791e3f6820c} + Perspex.Controls + + + {7062ae20-5dcc-4442-9645-8195bdece63e} + Perspex.Diagnostics + + + {62024b2d-53eb-4638-b26b-85eeaa54866e} + Perspex.Input + + + {6b0ed19d-a08b-461c-a9d9-a9ee40b0c06b} + Perspex.Interactivity + + + {42472427-4774-4c81-8aff-9f27b8e31721} + Perspex.Layout + + + {6417b24e-49c2-4985-8db2-3ab9d898ec91} + Perspex.ReactiveUI + + + {eb582467-6abb-43a1-b052-e981ba910e3a} + Perspex.SceneGraph + + + {f1baa01a-f176-4c6a-b39d-5b40bb1b148f} + Perspex.Styling + + + {3e10a5fa-e8da-48b1-ad44-6a5b6cb7750f} + Perspex.Themes.Default + + + {3e908f67-5543-4879-a1dc-08eace79b3cd} + Perspex.Direct2D1 + + + {811a76cf-1cf6-440f-963b-bbe31bd72a82} + Perspex.Win32 + + + + + \ No newline at end of file diff --git a/samples/BindingTest/MainWindow.paml b/samples/BindingTest/MainWindow.paml new file mode 100644 index 0000000000..739651b21e --- /dev/null +++ b/samples/BindingTest/MainWindow.paml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + !BooleanString + !!BooleanString + + + \ No newline at end of file diff --git a/samples/BindingTest/MainWindow.paml.cs b/samples/BindingTest/MainWindow.paml.cs new file mode 100644 index 0000000000..a8e64485cb --- /dev/null +++ b/samples/BindingTest/MainWindow.paml.cs @@ -0,0 +1,21 @@ +using BindingTest.ViewModels; +using Perspex.Controls; +using Perspex.Markup.Xaml; + +namespace BindingTest +{ + public class MainWindow : Window + { + public MainWindow() + { + this.InitializeComponent(); + this.DataContext = new MainWindowViewModel(); + App.AttachDevTools(this); + } + + private void InitializeComponent() + { + PerspexXamlLoader.Load(this); + } + } +} diff --git a/samples/BindingTest/Properties/AssemblyInfo.cs b/samples/BindingTest/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..648b1cb406 --- /dev/null +++ b/samples/BindingTest/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("BindingTest")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("BindingTest")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[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("08b3e6b9-1cd5-443c-9f61-6d49d1c5f162")] + +// 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/BindingTest/ViewModels/MainWindowViewModel.cs b/samples/BindingTest/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000000..8722568aee --- /dev/null +++ b/samples/BindingTest/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.ObjectModel; +using ReactiveUI; + +namespace BindingTest.ViewModels +{ + public class MainWindowViewModel : ReactiveObject + { + private string _booleanString = "True"; + private string _stringValue = "Simple Binding"; + + public MainWindowViewModel() + { + Items = new ObservableCollection + { + new TestItem { StringValue = "Foo" }, + new TestItem { StringValue = "Bar" }, + new TestItem { StringValue = "Baz" }, + }; + + ShuffleItems = ReactiveCommand.Create(); + ShuffleItems.Subscribe(_ => + { + var r = new Random(); + Items.Move(r.Next(Items.Count), 1); + }); + } + + public ObservableCollection Items { get; } + public ReactiveCommand ShuffleItems { get; } + + public string BooleanString + { + get { return _booleanString; } + set { this.RaiseAndSetIfChanged(ref _booleanString, value); } + } + + public string StringValue + { + get { return _stringValue; } + set { this.RaiseAndSetIfChanged(ref _stringValue, value); } + } + } +} diff --git a/samples/BindingTest/ViewModels/TestItem.cs b/samples/BindingTest/ViewModels/TestItem.cs new file mode 100644 index 0000000000..759f8ac974 --- /dev/null +++ b/samples/BindingTest/ViewModels/TestItem.cs @@ -0,0 +1,15 @@ +using ReactiveUI; + +namespace BindingTest.ViewModels +{ + public class TestItem : ReactiveObject + { + private string stringValue = "String Value"; + + public string StringValue + { + get { return stringValue; } + set { this.RaiseAndSetIfChanged(ref this.stringValue, value); } + } + } +} diff --git a/samples/BindingTest/packages.config b/samples/BindingTest/packages.config new file mode 100644 index 0000000000..2bac0ad760 --- /dev/null +++ b/samples/BindingTest/packages.config @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/samples/TestApplication/GalleryStyle.cs b/samples/TestApplication/GalleryStyle.cs index 1076e2cd0f..6bd137060a 100644 --- a/samples/TestApplication/GalleryStyle.cs +++ b/samples/TestApplication/GalleryStyle.cs @@ -61,7 +61,7 @@ namespace TestApplication { DataTemplates = new DataTemplates { - new DataTemplate(x => new Border + new FuncDataTemplate(x => new Border { [~Border.BackgroundProperty] = control[~TemplatedControl.BackgroundProperty], Padding = new Thickness(10), diff --git a/samples/TestApplication/Program.cs b/samples/TestApplication/Program.cs index 0b8a6134c7..0df84054fc 100644 --- a/samples/TestApplication/Program.cs +++ b/samples/TestApplication/Program.cs @@ -86,7 +86,7 @@ namespace TestApplication { DataTemplates = new DataTemplates { - new TreeDataTemplate( + new FuncTreeDataTemplate( x => new TextBlock { Text = x.Name }, x => x.Children, x => true), @@ -405,7 +405,7 @@ namespace TestApplication Margin = new Thickness(10), DataTemplates = new DataTemplates { - new DataTemplate(x => + new FuncDataTemplate(x => new StackPanel { Gap = 4, diff --git a/samples/XamlTestApplication/Program.cs b/samples/XamlTestApplication/Program.cs index e9421802bf..1f1517d1f6 100644 --- a/samples/XamlTestApplication/Program.cs +++ b/samples/XamlTestApplication/Program.cs @@ -23,10 +23,10 @@ namespace XamlTestApplication { }; - + var window = new MainWindow(); window.Show(); Application.Current.Run(window); } } -} +} \ No newline at end of file diff --git a/samples/XamlTestApplicationPcl/ViewModels/MainWindowViewModel.cs b/samples/XamlTestApplicationPcl/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000000..1523299d41 --- /dev/null +++ b/samples/XamlTestApplicationPcl/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,58 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Collections.Generic; + +namespace XamlTestApplication.ViewModels +{ + public class MainWindowViewModel + { + public MainWindowViewModel() + { + Items = new List(); + + for (int i = 0; i < 10; ++i) + { + Items.Add(new TestItem($"Item {i}", $"Item {i} Value")); + } + + Nodes = new List + { + new TestNode + { + Header = "Root", + SubHeader = "Root Item", + Children = new[] + { + new TestNode + { + Header = "Child 1", + SubHeader = "Child 1 Value", + }, + new TestNode + { + Header = "Child 2", + SubHeader = "Child 2 Value", + Children = new[] + { + new TestNode + { + Header = "Grandchild", + SubHeader = "Grandchild Value", + }, + new TestNode + { + Header = "Grandmaster Flash", + SubHeader = "White Lines", + }, + } + }, + } + } + }; + } + + public List Items { get; } + public List Nodes { get; } + } +} diff --git a/samples/XamlTestApplicationPcl/ViewModels/TestItem.cs b/samples/XamlTestApplicationPcl/ViewModels/TestItem.cs new file mode 100644 index 0000000000..6b711be54f --- /dev/null +++ b/samples/XamlTestApplicationPcl/ViewModels/TestItem.cs @@ -0,0 +1,17 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +namespace XamlTestApplication.ViewModels +{ + public class TestItem + { + public TestItem(string header, string subheader) + { + Header = header; + SubHeader = subheader; + } + + public string Header { get; } + public string SubHeader { get; } + } +} diff --git a/samples/XamlTestApplicationPcl/ViewModels/TestNode.cs b/samples/XamlTestApplicationPcl/ViewModels/TestNode.cs new file mode 100644 index 0000000000..953bfd0f58 --- /dev/null +++ b/samples/XamlTestApplicationPcl/ViewModels/TestNode.cs @@ -0,0 +1,14 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Collections.Generic; + +namespace XamlTestApplication.ViewModels +{ + public class TestNode + { + public string Header { get; set; } + public string SubHeader { get; set; } + public IEnumerable Children { get; set; } + } +} \ No newline at end of file diff --git a/samples/XamlTestApplicationPcl/Views/MainWindow.cs b/samples/XamlTestApplicationPcl/Views/MainWindow.cs index dc218641bf..d18985a898 100644 --- a/samples/XamlTestApplicationPcl/Views/MainWindow.cs +++ b/samples/XamlTestApplicationPcl/Views/MainWindow.cs @@ -1,15 +1,10 @@ // Copyright (c) The Perspex 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.IO; -using System.Reflection; -using System.Resources; -using OmniXaml; using Perspex.Controls; using Perspex.Diagnostics; using Perspex.Markup.Xaml; +using XamlTestApplication.ViewModels; namespace XamlTestApplication.Views { @@ -18,7 +13,7 @@ namespace XamlTestApplication.Views public MainWindow() { InitializeComponent(); - + DataContext = new MainWindowViewModel(); DevTools.Attach(this); } diff --git a/samples/XamlTestApplicationPcl/Views/MainWindow.paml b/samples/XamlTestApplicationPcl/Views/MainWindow.paml index 0794d15aff..3b6433d5b4 100644 --- a/samples/XamlTestApplicationPcl/Views/MainWindow.paml +++ b/samples/XamlTestApplicationPcl/Views/MainWindow.paml @@ -1,6 +1,7 @@  @@ -48,30 +49,36 @@ - - - - - - - - - - - - - - - + + + - - + + + + + + + + + + + + + + + + + + - - + + - + + + diff --git a/samples/XamlTestApplicationPcl/XamlTestApplicationPcl.csproj b/samples/XamlTestApplicationPcl/XamlTestApplicationPcl.csproj index fcc0fdcc27..3cc33712f7 100644 --- a/samples/XamlTestApplicationPcl/XamlTestApplicationPcl.csproj +++ b/samples/XamlTestApplicationPcl/XamlTestApplicationPcl.csproj @@ -41,6 +41,9 @@ + + + diff --git a/src/Gtk/Perspex.Cairo/Media/DrawingContext.cs b/src/Gtk/Perspex.Cairo/Media/DrawingContext.cs index f8c74f3f8f..a1fee1a2e3 100644 --- a/src/Gtk/Perspex.Cairo/Media/DrawingContext.cs +++ b/src/Gtk/Perspex.Cairo/Media/DrawingContext.cs @@ -2,6 +2,7 @@ // 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; using System.Reactive.Disposables; using System.Runtime.InteropServices; @@ -16,7 +17,7 @@ namespace Perspex.Cairo.Media /// /// Draws using Direct2D1. /// - public class DrawingContext : IDrawingContext, IDisposable + public class DrawingContext : IDrawingContextImpl, IDisposable { /// /// The cairo context. @@ -30,7 +31,6 @@ namespace Perspex.Cairo.Media public DrawingContext(Cairo.Surface surface) { _context = new Cairo.Context(surface); - CurrentTransform = Matrix.Identity; } /// @@ -40,15 +40,23 @@ namespace Perspex.Cairo.Media public DrawingContext(Gdk.Drawable drawable) { _context = Gdk.CairoHelper.Create(drawable); - CurrentTransform = Matrix.Identity; } + + private Matrix _transform = Matrix.Identity; /// /// Gets the current transform of the drawing context. /// - public Matrix CurrentTransform + public Matrix Transform { - get; } + get { return _transform; } + set + { + _transform = value; + _context.Matrix = value.ToCairo(); + + } + } /// /// Ends a draw operation. @@ -131,28 +139,30 @@ namespace Perspex.Cairo.Media { var impl = geometry.PlatformImpl as StreamGeometryImpl; - using (var pop = PushTransform(impl.Transform)) + var oldMatrix = Transform; + Transform = impl.Transform * Transform; + + + if (brush != null) { _context.AppendPath(impl.Path); - - if (brush != null) + using (var b = SetBrush(brush, geometry.Bounds.Size)) { - using (var b = SetBrush(brush, geometry.Bounds.Size)) - { - if (pen != null) - _context.FillPreserve(); - else - _context.Fill(); - } + if (pen != null) + _context.FillPreserve(); + else + _context.Fill(); } } + Transform = oldMatrix; if (pen != null) { - using (var p = SetPen(pen, geometry.Bounds.Size)) - { - _context.Stroke(); - } + _context.AppendPath(impl.Path); + using (var p = SetPen(pen, geometry.Bounds.Size)) + { + _context.Stroke(); + } } } @@ -208,31 +218,37 @@ namespace Perspex.Cairo.Media /// /// The clip rectangle. /// A disposable used to undo the clip rectangle. - public IDisposable PushClip(Rect clip) + public void PushClip(Rect clip) { _context.Save(); _context.Rectangle(clip.ToCairo()); _context.Clip(); + } - return Disposable.Create(() => _context.Restore()); + public void PopClip() + { + _context.Restore(); } + readonly Stack _opacityStack = new Stack(); + /// /// Pushes an opacity value. /// /// The opacity. /// A disposable used to undo the opacity. - public IDisposable PushOpacity(double opacity) + public void PushOpacity(double opacity) { - var tmp = opacityOverride; + _opacityStack.Push(opacityOverride); if (opacity < 1.0f) - opacityOverride = opacity; + opacityOverride *= opacity; - return Disposable.Create(() => - { - opacityOverride = tmp; - }); + } + + public void PopOpacity() + { + opacityOverride = _opacityStack.Pop(); } /// diff --git a/src/Gtk/Perspex.Cairo/Media/Imaging/RenderTargetBitmapImpl.cs b/src/Gtk/Perspex.Cairo/Media/Imaging/RenderTargetBitmapImpl.cs index 0b9183d928..4d8f05848a 100644 --- a/src/Gtk/Perspex.Cairo/Media/Imaging/RenderTargetBitmapImpl.cs +++ b/src/Gtk/Perspex.Cairo/Media/Imaging/RenderTargetBitmapImpl.cs @@ -39,7 +39,7 @@ namespace Perspex.Cairo.Media.Imaging Surface.WriteToPng(fileName); } - public IDrawingContext CreateDrawingContext() + public Perspex.Media.DrawingContext CreateDrawingContext() { return _renderTarget.CreateDrawingContext(); } diff --git a/src/Gtk/Perspex.Cairo/Media/TileBrushes.cs b/src/Gtk/Perspex.Cairo/Media/TileBrushes.cs index 94fb19ea0b..98361427f0 100644 --- a/src/Gtk/Perspex.Cairo/Media/TileBrushes.cs +++ b/src/Gtk/Perspex.Cairo/Media/TileBrushes.cs @@ -113,7 +113,7 @@ namespace Perspex.Cairo.Media out drawRect); using (ctx.PushClip(drawRect)) - using (ctx.PushTransform(transform)) + using (ctx.PushPostTransform(transform)) { ctx.Render(visual); } diff --git a/src/Gtk/Perspex.Cairo/RenderTarget.cs b/src/Gtk/Perspex.Cairo/RenderTarget.cs index 333c558e8a..2daab33fbe 100644 --- a/src/Gtk/Perspex.Cairo/RenderTarget.cs +++ b/src/Gtk/Perspex.Cairo/RenderTarget.cs @@ -7,6 +7,7 @@ using Perspex.Cairo.Media; using Perspex.Media; using Perspex.Platform; using Perspex.Rendering; +using DrawingContext = Perspex.Media.DrawingContext; namespace Perspex.Cairo { @@ -50,12 +51,13 @@ namespace Perspex.Cairo /// /// Creates a cairo surface that targets a platform-specific resource. /// - /// A surface wrapped in an . - public IDrawingContext CreateDrawingContext() + /// A surface wrapped in an . + public DrawingContext CreateDrawingContext() { - if(_surface != null) - return new DrawingContext(_surface); - return new DrawingContext(_window.GdkWindow); + var ctx = _surface != null + ? new Media.DrawingContext(_surface) + : new Media.DrawingContext(_window.GdkWindow); + return new DrawingContext(ctx); } public void Dispose() => _surface?.Dispose(); diff --git a/src/Markup/Perspex.Markup.Xaml/DataBinding/SourceBindingEndpoint.cs b/src/Markup/Perspex.Markup.Xaml/Binding/SourceBindingEndpoint.cs similarity index 94% rename from src/Markup/Perspex.Markup.Xaml/DataBinding/SourceBindingEndpoint.cs rename to src/Markup/Perspex.Markup.Xaml/Binding/SourceBindingEndpoint.cs index 3ed6304049..60b03f31ce 100644 --- a/src/Markup/Perspex.Markup.Xaml/DataBinding/SourceBindingEndpoint.cs +++ b/src/Markup/Perspex.Markup.Xaml/Binding/SourceBindingEndpoint.cs @@ -4,7 +4,7 @@ using System; using System.ComponentModel; -namespace Perspex.Markup.Xaml.DataBinding +namespace Perspex.Markup.Xaml.Binding { public class SourceBindingEndpoint { diff --git a/src/Markup/Perspex.Markup.Xaml/DataBinding/TargetBindingEndpoint.cs b/src/Markup/Perspex.Markup.Xaml/Binding/TargetBindingEndpoint.cs similarity index 91% rename from src/Markup/Perspex.Markup.Xaml/DataBinding/TargetBindingEndpoint.cs rename to src/Markup/Perspex.Markup.Xaml/Binding/TargetBindingEndpoint.cs index aff0f63ec6..316dcddcc4 100644 --- a/src/Markup/Perspex.Markup.Xaml/DataBinding/TargetBindingEndpoint.cs +++ b/src/Markup/Perspex.Markup.Xaml/Binding/TargetBindingEndpoint.cs @@ -1,7 +1,7 @@ // Copyright (c) The Perspex Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -namespace Perspex.Markup.Xaml.DataBinding +namespace Perspex.Markup.Xaml.Binding { public class TargetBindingEndpoint { diff --git a/src/Markup/Perspex.Markup.Xaml/Binding/XamlBinding.cs b/src/Markup/Perspex.Markup.Xaml/Binding/XamlBinding.cs new file mode 100644 index 0000000000..cb2c999f7a --- /dev/null +++ b/src/Markup/Perspex.Markup.Xaml/Binding/XamlBinding.cs @@ -0,0 +1,96 @@ +// Copyright (c) The Perspex 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 OmniXaml.TypeConversion; +using Perspex.Controls; +using Perspex.Markup.Binding; + +namespace Perspex.Markup.Xaml.Binding +{ + public class XamlBinding + { + private readonly ITypeConverterProvider _typeConverterProvider; + + public XamlBinding() + { + } + + public XamlBinding(ITypeConverterProvider typeConverterProvider) + { + _typeConverterProvider = typeConverterProvider; + } + + public string SourcePropertyPath { get; set; } + + public BindingMode BindingMode { get; set; } + + public void Bind(IObservablePropertyBag instance, PerspexProperty property) + { + var subject = new ExpressionSubject(CreateExpressionObserver(instance, property)); + + if (subject != null) + { + Bind(instance, property, subject); + } + } + + public ExpressionObserver CreateExpressionObserver( + IObservablePropertyBag instance, + PerspexProperty property) + { + IObservable dataContext = null; + + if (property != Control.DataContextProperty) + { + dataContext = instance.GetObservable(Control.DataContextProperty); + } + else + { + var parent = instance.InheritanceParent as IObservablePropertyBag; + + if (parent != null) + { + dataContext = parent.GetObservable(Control.DataContextProperty); + } + } + + if (dataContext != null) + { + var result = new ExpressionObserver(null, SourcePropertyPath); + dataContext.Subscribe(x => result.Root = x); + return result; + } + + return null; + } + + internal void Bind(IObservablePropertyBag target, PerspexProperty property, ISubject subject) + { + var mode = BindingMode == BindingMode.Default ? + property.DefaultBindingMode : BindingMode; + + switch (mode) + { + case BindingMode.Default: + case BindingMode.OneWay: + target.Bind(property, subject); + break; + case BindingMode.TwoWay: + target.BindTwoWay(property, subject); + break; + case BindingMode.OneTime: + target.GetObservable(Control.DataContextProperty).Subscribe(dataContext => + { + subject.Take(1).Subscribe(x => target.SetValue(property, x)); + }); + break; + case BindingMode.OneWayToSource: + target.GetObservable(property).Subscribe(subject); + break; + } + } + } +} \ No newline at end of file diff --git a/src/Markup/Perspex.Markup.Xaml/Binding/XamlBindingDefinition.cs b/src/Markup/Perspex.Markup.Xaml/Binding/XamlBindingDefinition.cs new file mode 100644 index 0000000000..c571036d4d --- /dev/null +++ b/src/Markup/Perspex.Markup.Xaml/Binding/XamlBindingDefinition.cs @@ -0,0 +1,21 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Perspex.Controls; + +namespace Perspex.Markup.Xaml.Binding +{ + public class XamlBindingDefinition + { + public XamlBindingDefinition( + string sourcePropertyPath, + BindingMode bindingMode) + { + SourcePropertyPath = sourcePropertyPath; + BindingMode = bindingMode; + } + + public string SourcePropertyPath { get; } + public BindingMode BindingMode { get; } + } +} \ No newline at end of file diff --git a/src/Markup/Perspex.Markup.Xaml/Context/PerspexObjectAssembler.cs b/src/Markup/Perspex.Markup.Xaml/Context/PerspexObjectAssembler.cs index b51aae5969..25df85a3cc 100644 --- a/src/Markup/Perspex.Markup.Xaml/Context/PerspexObjectAssembler.cs +++ b/src/Markup/Perspex.Markup.Xaml/Context/PerspexObjectAssembler.cs @@ -15,7 +15,8 @@ namespace Perspex.Markup.Xaml.Context public PerspexObjectAssembler(IWiringContext wiringContext, ObjectAssemblerSettings objectAssemblerSettings = null) { var mapping = new DeferredLoaderMapping(); - mapping.Map(template => template.Content, new TemplateLoader()); + mapping.Map(template => template.Content, new TemplateLoader()); + mapping.Map(template => template.Content, new TemplateLoader()); var assembler = new ObjectAssembler(wiringContext, new TopDownValueContext(), objectAssemblerSettings); _objectAssembler = new TemplateHostingObjectAssembler(assembler, mapping); diff --git a/src/Markup/Perspex.Markup.Xaml/Context/PerspexTypeRepository.cs b/src/Markup/Perspex.Markup.Xaml/Context/PerspexTypeRepository.cs index 4d32ee8658..44dda9b9f9 100644 --- a/src/Markup/Perspex.Markup.Xaml/Context/PerspexTypeRepository.cs +++ b/src/Markup/Perspex.Markup.Xaml/Context/PerspexTypeRepository.cs @@ -5,28 +5,25 @@ using System; using Glass; using OmniXaml; using OmniXaml.Typing; -using Perspex.Markup.Xaml.DataBinding; +using Perspex.Markup.Xaml.Binding; namespace Perspex.Markup.Xaml.Context { public class PerspexTypeRepository : XamlTypeRepository { private readonly ITypeFactory _typeFactory; - private readonly IPerspexPropertyBinder _propertyBinder; public PerspexTypeRepository(IXamlNamespaceRegistry xamlNamespaceRegistry, ITypeFactory typeFactory, - ITypeFeatureProvider featureProvider, - IPerspexPropertyBinder propertyBinder) : base(xamlNamespaceRegistry, typeFactory, featureProvider) + ITypeFeatureProvider featureProvider) : base(xamlNamespaceRegistry, typeFactory, featureProvider) { _typeFactory = typeFactory; - _propertyBinder = propertyBinder; } public override XamlType GetXamlType(Type type) { Guard.ThrowIfNull(type, nameof(type)); - return new PerspexXamlType(type, this, _typeFactory, FeatureProvider, _propertyBinder); + return new PerspexXamlType(type, this, _typeFactory, FeatureProvider); } } } \ No newline at end of file diff --git a/src/Markup/Perspex.Markup.Xaml/Context/PerspexWiringContext.cs b/src/Markup/Perspex.Markup.Xaml/Context/PerspexWiringContext.cs index 12ef408fcd..3e5b5c24c2 100644 --- a/src/Markup/Perspex.Markup.Xaml/Context/PerspexWiringContext.cs +++ b/src/Markup/Perspex.Markup.Xaml/Context/PerspexWiringContext.cs @@ -15,7 +15,7 @@ using Perspex.Controls; using Perspex.Input; using Perspex.Markup.Xaml.Templates; using Perspex.Markup.Xaml.Converters; -using Perspex.Markup.Xaml.DataBinding; +using Perspex.Markup.Xaml.Binding; using Perspex.Markup.Xaml.MarkupExtensions; using Perspex.Media; using Perspex.Media.Imaging; @@ -43,8 +43,7 @@ namespace Perspex.Markup.Xaml.Context private static ITypeContext CreateTypeContext(ITypeFactory typeFactory, TypeFeatureProvider featureProvider) { var xamlNamespaceRegistry = CreateXamlNamespaceRegistry(); - var perspexPropertyBinder = new PerspexPropertyBinder(featureProvider.ConverterProvider); - var typeRepository = new PerspexTypeRepository(xamlNamespaceRegistry, typeFactory, featureProvider, perspexPropertyBinder); + var typeRepository = new PerspexTypeRepository(xamlNamespaceRegistry, typeFactory, featureProvider); typeRepository.RegisterMetadata(new Metadata().WithMemberDependency(x => x.Value, x => x.Property)); typeRepository.RegisterMetadata( @@ -116,6 +115,7 @@ namespace Perspex.Markup.Xaml.Context var contentProperties = new Collection { new ContentPropertyDefinition(typeof(ContentControl), "Content"), + new ContentPropertyDefinition(typeof(DataTemplate), "Content"), new ContentPropertyDefinition(typeof(Decorator), "Child"), new ContentPropertyDefinition(typeof(ItemsControl), "Items"), new ContentPropertyDefinition(typeof(GradientBrush), "GradientStops"), @@ -123,7 +123,7 @@ namespace Perspex.Markup.Xaml.Context new ContentPropertyDefinition(typeof(Style), "Setters"), new ContentPropertyDefinition(typeof(TextBlock), "Text"), new ContentPropertyDefinition(typeof(TextBox), "Text"), - new ContentPropertyDefinition(typeof(XamlDataTemplate), "Content"), + new ContentPropertyDefinition(typeof(TreeDataTemplate), "Content"), }; contentPropertyProvider.AddAll(contentProperties); diff --git a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMember.cs b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMember.cs index dc4c4cbde2..89979885f0 100644 --- a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMember.cs +++ b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMember.cs @@ -1,7 +1,7 @@ // Copyright (c) The Perspex Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using Perspex.Markup.Xaml.DataBinding; +using Perspex.Markup.Xaml.Binding; using OmniXaml; using OmniXaml.Typing; @@ -9,21 +9,17 @@ namespace Perspex.Markup.Xaml.Context { public class PerspexXamlMember : XamlMember { - private readonly IPerspexPropertyBinder _propertyBinder; - public PerspexXamlMember(string name, XamlType owner, IXamlTypeRepository xamlTypeRepository, - ITypeFeatureProvider featureProvider, - IPerspexPropertyBinder propertyBinder) + ITypeFeatureProvider featureProvider) : base(name, owner, xamlTypeRepository, featureProvider) { - _propertyBinder = propertyBinder; } protected override IXamlMemberValuePlugin LookupXamlMemberValueConnector() { - return new PerspexXamlMemberValuePlugin(this, _propertyBinder); + return new PerspexXamlMemberValuePlugin(this); } public override string ToString() diff --git a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs index 423ca66e23..fd7a786649 100644 --- a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs +++ b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs @@ -2,12 +2,14 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Linq; using System.Reactive.Linq; +using System.Reflection; using Glass; using OmniXaml.ObjectAssembler; using OmniXaml.Typing; using Perspex.Controls; -using Perspex.Markup.Xaml.DataBinding; +using Perspex.Markup.Xaml.Binding; using Perspex.Styling; namespace Perspex.Markup.Xaml.Context @@ -15,19 +17,18 @@ namespace Perspex.Markup.Xaml.Context public class PerspexXamlMemberValuePlugin : MemberValuePlugin { private readonly XamlMember _xamlMember; - private readonly IPerspexPropertyBinder _propertyBinder; - public PerspexXamlMemberValuePlugin(XamlMember xamlMember, IPerspexPropertyBinder propertyBinder) : base(xamlMember) + public PerspexXamlMemberValuePlugin(XamlMember xamlMember) + : base(xamlMember) { _xamlMember = xamlMember; - _propertyBinder = propertyBinder; } public override void SetValue(object instance, object value) { if (value is XamlBindingDefinition) { - HandleXamlBindingDefinition((XamlBindingDefinition)value); + HandleXamlBindingDefinition(instance, (XamlBindingDefinition)value); } else if (IsPerspexProperty) { @@ -54,22 +55,48 @@ namespace Perspex.Markup.Xaml.Context po.SetValue(pp, value); } - private void HandleXamlBindingDefinition(XamlBindingDefinition xamlBindingDefinition) + private void HandleXamlBindingDefinition(object instance, XamlBindingDefinition def) { - PerspexObject subjectObject = xamlBindingDefinition.Target; - _propertyBinder.Create(xamlBindingDefinition); + if (_xamlMember.XamlType.UnderlyingType == typeof(XamlBindingDefinition)) + { + // TODO: This should search base classes. + var property = instance.GetType().GetTypeInfo().GetDeclaredProperty(_xamlMember.Name); - var observableForDataContext = subjectObject.GetObservable(Control.DataContextProperty); - observableForDataContext.Where(o => o != null).Subscribe(_ => BindToDataContextWhenItsSet(xamlBindingDefinition)); - } + if (property == null || !property.CanWrite) + { + throw new InvalidOperationException( + $"Cannot assign to '{_xamlMember.Name}' on '{instance.GetType()}"); + } - private void BindToDataContextWhenItsSet(XamlBindingDefinition definition) - { - var target = definition.Target; - var dataContext = target.DataContext; + property.SetValue(instance, def); + } + else + { + var perspexObject = instance as PerspexObject; - var binding = _propertyBinder.GetBinding(target, definition.TargetProperty); - binding.BindToDataContext(dataContext); + if (perspexObject == null) + { + throw new InvalidOperationException( + $"Cannot bind to an object of type '{instance.GetType()}"); + } + + var property = perspexObject.GetRegisteredProperties() + .FirstOrDefault(x => x.Name == _xamlMember.Name); + + if (property == null) + { + throw new InvalidOperationException( + $"Cannot find '{_xamlMember.Name}' on '{instance.GetType()}"); + } + + var binding = new XamlBinding + { + BindingMode = def.BindingMode, + SourcePropertyPath = def.SourcePropertyPath, + }; + + binding.Bind(perspexObject, property); + } } // ReSharper disable once MemberCanBePrivate.Global diff --git a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlType.cs b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlType.cs index 827f6f3827..0d1fb218b1 100644 --- a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlType.cs +++ b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlType.cs @@ -4,28 +4,22 @@ using System; using OmniXaml; using OmniXaml.Typing; -using Perspex.Markup.Xaml.DataBinding; +using Perspex.Markup.Xaml.Binding; namespace Perspex.Markup.Xaml.Context { public class PerspexXamlType : XamlType { - private readonly IPerspexPropertyBinder _propertyBinder; - public PerspexXamlType(Type type, IXamlTypeRepository typeRepository, ITypeFactory typeFactory, - ITypeFeatureProvider featureProvider, - IPerspexPropertyBinder propertyBinder) : base(type, typeRepository, typeFactory, featureProvider) + ITypeFeatureProvider featureProvider) : base(type, typeRepository, typeFactory, featureProvider) { - _propertyBinder = propertyBinder; } - protected IPerspexPropertyBinder PropertyBinder => _propertyBinder; - protected override XamlMember LookupMember(string name) { - return new PerspexXamlMember(name, this, TypeRepository, FeatureProvider, _propertyBinder); + return new PerspexXamlMember(name, this, TypeRepository, FeatureProvider); } public override string ToString() diff --git a/src/Markup/Perspex.Markup.Xaml/DataBinding/ChangeTracking/ObservablePropertyBranch.cs b/src/Markup/Perspex.Markup.Xaml/DataBinding/ChangeTracking/ObservablePropertyBranch.cs deleted file mode 100644 index 2756065280..0000000000 --- a/src/Markup/Perspex.Markup.Xaml/DataBinding/ChangeTracking/ObservablePropertyBranch.cs +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (c) The Perspex 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; -using System.Linq; -using System.Reactive.Linq; -using System.Reflection; -using Glass; - -namespace Perspex.Markup.Xaml.DataBinding.ChangeTracking -{ - public class ObservablePropertyBranch - { - private readonly object _instance; - private readonly PropertyPath _propertyPath; - private readonly PropertyMountPoint _mountPoint; - - public ObservablePropertyBranch(object instance, PropertyPath propertyPath) - { - Guard.ThrowIfNull(instance, nameof(instance)); - Guard.ThrowIfNull(propertyPath, nameof(propertyPath)); - - _instance = instance; - _propertyPath = propertyPath; - _mountPoint = new PropertyMountPoint(instance, propertyPath); - var properties = GetPropertiesThatRaiseNotifications(); - Values = CreateUnifiedObservableFromNodes(properties); - } - - public IObservable Values { get; private set; } - - private IObservable CreateUnifiedObservableFromNodes(IEnumerable subscriptions) - { - return subscriptions.Select(GetObservableFromProperty).Merge(); - } - - private IObservable GetObservableFromProperty(PropertyDefinition subscription) - { - return Observable.FromEventPattern( - parentOnPropertyChanged => subscription.Parent.PropertyChanged += parentOnPropertyChanged, - parentOnPropertyChanged => subscription.Parent.PropertyChanged -= parentOnPropertyChanged) - .Where(pattern => pattern.EventArgs.PropertyName == subscription.PropertyName) - .Select(pattern => _mountPoint.Value); - } - - private IEnumerable GetPropertiesThatRaiseNotifications() - { - return GetSubscriptionsRecursive(_instance, _propertyPath, 0); - } - - private IEnumerable GetSubscriptionsRecursive(object current, PropertyPath propertyPath, int i) - { - var subscriptions = new List(); - var inpc = current as INotifyPropertyChanged; - - if (inpc == null) - { - return subscriptions; - } - - var nextPropertyName = propertyPath.Chunks[i]; - subscriptions.Add(new PropertyDefinition(inpc, nextPropertyName)); - - if (i < _propertyPath.Chunks.Length) - { - var currentObjectTypeInfo = current.GetType().GetTypeInfo(); - var nextProperty = currentObjectTypeInfo.GetDeclaredProperty(nextPropertyName); - var nextInstance = nextProperty.GetValue(current); - - if (i < _propertyPath.Chunks.Length - 1) - { - subscriptions.AddRange(GetSubscriptionsRecursive(nextInstance, propertyPath, i + 1)); - } - } - - return subscriptions; - } - - public object Value - { - get - { - return _mountPoint.Value; - } - - set - { - _mountPoint.Value = value; - } - } - - public Type Type => _mountPoint.ProperyType; - - private class PropertyDefinition - { - public PropertyDefinition(INotifyPropertyChanged parent, string propertyName) - { - Parent = parent; - PropertyName = propertyName; - } - - public INotifyPropertyChanged Parent { get; } - - public string PropertyName { get; } - } - } -} \ No newline at end of file diff --git a/src/Markup/Perspex.Markup.Xaml/DataBinding/ChangeTracking/PropertyMountPoint.cs b/src/Markup/Perspex.Markup.Xaml/DataBinding/ChangeTracking/PropertyMountPoint.cs deleted file mode 100644 index 8ea903fa55..0000000000 --- a/src/Markup/Perspex.Markup.Xaml/DataBinding/ChangeTracking/PropertyMountPoint.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) The Perspex 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.Reflection; -using Glass; - -namespace Perspex.Markup.Xaml.DataBinding.ChangeTracking -{ - public class PropertyMountPoint - { - private readonly TargettedProperty _referencedTargettedProperty; - - public PropertyMountPoint(object origin, PropertyPath propertyPath) - { - Guard.ThrowIfNull(origin, nameof(origin)); - Guard.ThrowIfNull(propertyPath, nameof(propertyPath)); - - _referencedTargettedProperty = GetReferencedPropertyInfo(origin, propertyPath, 0); - } - - private static TargettedProperty GetReferencedPropertyInfo(object current, PropertyPath propertyPath, int level) - { - var typeInfo = current.GetType().GetTypeInfo(); - var leftPropertyInfo = typeInfo.GetDeclaredProperty(propertyPath.Chunks[level]); - - if (level == propertyPath.Chunks.Length - 1) - { - return new TargettedProperty(current, leftPropertyInfo); - } - - var nextInstance = leftPropertyInfo.GetValue(current); - - return GetReferencedPropertyInfo(nextInstance, propertyPath, level + 1); - } - - public object Value - { - get - { - return _referencedTargettedProperty.Value; - } - - set - { - _referencedTargettedProperty.Value = value; - } - } - - public Type ProperyType => _referencedTargettedProperty.PropertyType; - } -} \ No newline at end of file diff --git a/src/Markup/Perspex.Markup.Xaml/DataBinding/ChangeTracking/PropertyPath.cs b/src/Markup/Perspex.Markup.Xaml/DataBinding/ChangeTracking/PropertyPath.cs deleted file mode 100644 index 3558645b1b..0000000000 --- a/src/Markup/Perspex.Markup.Xaml/DataBinding/ChangeTracking/PropertyPath.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) The Perspex Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -namespace Perspex.Markup.Xaml.DataBinding.ChangeTracking -{ - public class PropertyPath - { - private string[] _chunks; - - private PropertyPath(PropertyPath propertyPath) - { - _chunks = propertyPath.Chunks; - } - - public PropertyPath(string path) - { - _chunks = path.Split('.'); - } - - public string[] Chunks - { - get { return _chunks; } - set { _chunks = value; } - } - - public PropertyPath Clone() - { - return new PropertyPath(this); - } - - public override string ToString() - { - return string.Join(".", _chunks); - } - } -} \ No newline at end of file diff --git a/src/Markup/Perspex.Markup.Xaml/DataBinding/ChangeTracking/TargettedProperty.cs b/src/Markup/Perspex.Markup.Xaml/DataBinding/ChangeTracking/TargettedProperty.cs deleted file mode 100644 index fec5edc904..0000000000 --- a/src/Markup/Perspex.Markup.Xaml/DataBinding/ChangeTracking/TargettedProperty.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) The Perspex 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.Reflection; -using Glass; - -namespace Perspex.Markup.Xaml.DataBinding.ChangeTracking -{ - internal class TargettedProperty - { - private readonly object _instance; - private readonly PropertyInfo _propertyInfo; - - public TargettedProperty(object instance, PropertyInfo propertyInfo) - { - Guard.ThrowIfNull(instance, nameof(instance)); - Guard.ThrowIfNull(propertyInfo, nameof(propertyInfo)); - - _instance = instance; - _propertyInfo = propertyInfo; - } - - public object Value - { - get - { - return _propertyInfo.GetValue(_instance); - } - - set - { - _propertyInfo.SetValue(_instance, value); - } - } - - public Type PropertyType => _propertyInfo.PropertyType; - - public string Name => _propertyInfo.Name; - } -} \ No newline at end of file diff --git a/src/Markup/Perspex.Markup.Xaml/DataBinding/DataContextChangeSynchronizer.cs b/src/Markup/Perspex.Markup.Xaml/DataBinding/DataContextChangeSynchronizer.cs deleted file mode 100644 index 27723b5f75..0000000000 --- a/src/Markup/Perspex.Markup.Xaml/DataBinding/DataContextChangeSynchronizer.cs +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright (c) The Perspex 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.Reflection; -using Glass; -using OmniXaml.TypeConversion; -using Perspex.Markup.Xaml.DataBinding.ChangeTracking; - -namespace Perspex.Markup.Xaml.DataBinding -{ - public class DataContextChangeSynchronizer - { - private readonly BindingTarget _bindingTarget; - private readonly ITypeConverter _targetPropertyTypeConverter; - private readonly TargetBindingEndpoint _bindingEndpoint; - private readonly ObservablePropertyBranch _sourceEndpoint; - - public DataContextChangeSynchronizer(BindingSource bindingSource, BindingTarget bindingTarget, ITypeConverterProvider typeConverterProvider) - { - _bindingTarget = bindingTarget; - Guard.ThrowIfNull(bindingTarget.Object, nameof(bindingTarget.Object)); - Guard.ThrowIfNull(bindingTarget.Property, nameof(bindingTarget.Property)); - Guard.ThrowIfNull(bindingSource.SourcePropertyPath, nameof(bindingSource.SourcePropertyPath)); - Guard.ThrowIfNull(bindingSource.Source, nameof(bindingSource.Source)); - Guard.ThrowIfNull(typeConverterProvider, nameof(typeConverterProvider)); - - _bindingEndpoint = new TargetBindingEndpoint(bindingTarget.Object, bindingTarget.Property); - _sourceEndpoint = new ObservablePropertyBranch(bindingSource.Source, bindingSource.SourcePropertyPath); - _targetPropertyTypeConverter = typeConverterProvider.GetTypeConverter(bindingTarget.Property.PropertyType); - } - - public class BindingTarget - { - private readonly PerspexObject _obj; - private readonly PerspexProperty _property; - - public BindingTarget(PerspexObject @object, PerspexProperty property) - { - _obj = @object; - _property = property; - } - - public PerspexObject Object => _obj; - - public PerspexProperty Property => _property; - - public object Value - { - get { return _obj.GetValue(_property); } - set { _obj.SetValue(_property, value); } - } - } - - public class BindingSource - { - private readonly PropertyPath _sourcePropertyPath; - private readonly object _source; - - public BindingSource(PropertyPath sourcePropertyPath, object source) - { - _sourcePropertyPath = sourcePropertyPath; - _source = source; - } - - public PropertyPath SourcePropertyPath => _sourcePropertyPath; - - public object Source => _source; - } - - public void StartUpdatingTargetWhenSourceChanges() - { - // TODO: commenting out this line will make the existing value to be skipped from the SourceValues. This is not supposed to happen. Is it? - _bindingTarget.Value = ConvertedValue(_sourceEndpoint.Value, _bindingTarget.Property.PropertyType); - - // We use the native Bind method from PerspexObject to subscribe to the SourceValues observable - _bindingTarget.Object.Bind(_bindingTarget.Property, SourceValues); - } - - public void StartUpdatingSourceWhenTargetChanges() - { - // We subscribe to the TargetValues and each time we have a new value, we update the source with it - TargetValues.Subscribe(newValue => _sourceEndpoint.Value = newValue); - } - - private IObservable SourceValues - { - get - { - return _sourceEndpoint.Values.Select(originalValue => ConvertedValue(originalValue, _bindingTarget.Property.PropertyType)); - } - } - - private IObservable TargetValues - { - get - { - return _bindingEndpoint.Object - .GetObservable(_bindingEndpoint.Property).Select(o => ConvertedValue(o, _sourceEndpoint.Type)); - } - } - - private bool CanAssignWithoutConversion - { - get - { - var sourceTypeInfo = _sourceEndpoint.Type.GetTypeInfo(); - var targetTypeInfo = _bindingEndpoint.Property.PropertyType.GetTypeInfo(); - var compatible = targetTypeInfo.IsAssignableFrom(sourceTypeInfo); - return compatible; - } - } - - private object ConvertedValue(object originalValue, Type propertyType) - { - object converted; - if (TryConvert(originalValue, propertyType, out converted)) - { - return converted; - } - - return null; - } - - private bool TryConvert(object originalValue, Type targetType, out object finalValue) - { - if (originalValue != null) - { - if (CanAssignWithoutConversion) - { - finalValue = originalValue; - return true; - } - - if (_targetPropertyTypeConverter != null) - { - if (_targetPropertyTypeConverter.CanConvertTo(null, targetType)) - { - object convertedValue = _targetPropertyTypeConverter.ConvertTo( - null, - CultureInfo.InvariantCulture, - originalValue, - targetType); - - if (convertedValue != null) - { - finalValue = convertedValue; - return true; - } - } - } - } - else - { - finalValue = null; - return true; - } - - finalValue = null; - return false; - } - } -} \ No newline at end of file diff --git a/src/Markup/Perspex.Markup.Xaml/DataBinding/IPerspexPropertyBinder.cs b/src/Markup/Perspex.Markup.Xaml/DataBinding/IPerspexPropertyBinder.cs deleted file mode 100644 index d7879dc29b..0000000000 --- a/src/Markup/Perspex.Markup.Xaml/DataBinding/IPerspexPropertyBinder.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) The Perspex Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System.Collections.Generic; - -namespace Perspex.Markup.Xaml.DataBinding -{ - public interface IPerspexPropertyBinder - { - XamlBinding GetBinding(PerspexObject po, PerspexProperty pp); - - IEnumerable GetBindings(PerspexObject source); - - XamlBinding Create(XamlBindingDefinition xamlBinding); - } -} \ No newline at end of file diff --git a/src/Markup/Perspex.Markup.Xaml/DataBinding/PerspexPropertyBinder.cs b/src/Markup/Perspex.Markup.Xaml/DataBinding/PerspexPropertyBinder.cs deleted file mode 100644 index 27f93d53e9..0000000000 --- a/src/Markup/Perspex.Markup.Xaml/DataBinding/PerspexPropertyBinder.cs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) The Perspex 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; -using OmniXaml.TypeConversion; - -namespace Perspex.Markup.Xaml.DataBinding -{ - public class PerspexPropertyBinder : IPerspexPropertyBinder - { - private readonly ITypeConverterProvider _typeConverterProvider; - - private readonly HashSet _bindings; - - public PerspexPropertyBinder(ITypeConverterProvider typeConverterProvider) - { - _typeConverterProvider = typeConverterProvider; - _bindings = new HashSet(); - } - - public XamlBinding GetBinding(PerspexObject po, PerspexProperty pp) - { - return _bindings.First(xamlBinding => xamlBinding.Target == po && xamlBinding.TargetProperty == pp); - } - - public IEnumerable GetBindings(PerspexObject source) - { - return from binding in _bindings - where binding.Target == source - select binding; - } - - public XamlBinding Create(XamlBindingDefinition xamlBinding) - { - if (xamlBinding.Target == null) - { - throw new InvalidOperationException(); - } - - if (xamlBinding.TargetProperty == null) - { - throw new InvalidOperationException(); - } - - var binding = new XamlBinding(_typeConverterProvider) - { - BindingMode = xamlBinding.BindingMode, - SourcePropertyPath = xamlBinding.SourcePropertyPath, - Target = xamlBinding.Target, - TargetProperty = xamlBinding.TargetProperty - }; - - _bindings.Add(binding); - return binding; - } - } -} \ No newline at end of file diff --git a/src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBinding.cs b/src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBinding.cs deleted file mode 100644 index af0d33de6a..0000000000 --- a/src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBinding.cs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) The Perspex 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.Diagnostics; -using OmniXaml.TypeConversion; -using Perspex.Markup.Xaml.DataBinding.ChangeTracking; - -namespace Perspex.Markup.Xaml.DataBinding -{ - public class XamlBinding - { - private readonly ITypeConverterProvider _typeConverterProvider; - private DataContextChangeSynchronizer _changeSynchronizer; - - public XamlBinding(ITypeConverterProvider typeConverterProvider) - { - _typeConverterProvider = typeConverterProvider; - } - - public PerspexObject Target { get; set; } - - public PerspexProperty TargetProperty { get; set; } - - public PropertyPath SourcePropertyPath { get; set; } - - public BindingMode BindingMode { get; set; } - - public void BindToDataContext(object dataContext) - { - if (dataContext == null) - { - return; - } - - try - { - var bindingSource = new DataContextChangeSynchronizer.BindingSource(SourcePropertyPath, dataContext); - var bindingTarget = new DataContextChangeSynchronizer.BindingTarget(Target, TargetProperty); - - _changeSynchronizer = new DataContextChangeSynchronizer(bindingSource, bindingTarget, _typeConverterProvider); - - if (BindingMode == BindingMode.TwoWay) - { - _changeSynchronizer.StartUpdatingTargetWhenSourceChanges(); - _changeSynchronizer.StartUpdatingSourceWhenTargetChanges(); - } - - if (BindingMode == BindingMode.OneWay) - { - _changeSynchronizer.StartUpdatingTargetWhenSourceChanges(); - } - - if (BindingMode == BindingMode.OneWayToSource) - { - _changeSynchronizer.StartUpdatingSourceWhenTargetChanges(); - } - } - catch (Exception e) - { - Debug.WriteLine(e); - } - } - } -} \ No newline at end of file diff --git a/src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBindingDefinition.cs b/src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBindingDefinition.cs deleted file mode 100644 index cce86455d6..0000000000 --- a/src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBindingDefinition.cs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) The Perspex Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using Perspex.Controls; -using Perspex.Markup.Xaml.DataBinding.ChangeTracking; - -namespace Perspex.Markup.Xaml.DataBinding -{ - public class XamlBindingDefinition - { - private readonly PropertyPath _sourcePropertyPath; - private readonly BindingMode _bindingMode; - private readonly Control _target; - private readonly PerspexProperty _targetProperty; - - public XamlBindingDefinition(Control target, PerspexProperty targetProperty, PropertyPath sourcePropertyPath, BindingMode bindingMode) - { - _target = target; - _targetProperty = targetProperty; - _sourcePropertyPath = sourcePropertyPath; - _bindingMode = bindingMode; - } - - public Control Target => _target; - - public PerspexProperty TargetProperty => _targetProperty; - - public PropertyPath SourcePropertyPath => _sourcePropertyPath; - - public BindingMode BindingMode => _bindingMode; - } -} \ No newline at end of file diff --git a/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs index 9777ba3c9d..7abc09ef89 100644 --- a/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs @@ -4,8 +4,7 @@ using System.Linq; using OmniXaml; using Perspex.Controls; -using Perspex.Markup.Xaml.DataBinding; -using Perspex.Markup.Xaml.DataBinding.ChangeTracking; +using Perspex.Markup.Xaml.Binding; namespace Perspex.Markup.Xaml.MarkupExtensions { @@ -22,23 +21,10 @@ namespace Perspex.Markup.Xaml.MarkupExtensions public override object ProvideValue(MarkupExtensionContext extensionContext) { - var target = extensionContext.TargetObject as Control; - var targetProperty = extensionContext.TargetProperty; - var targetPropertyName = targetProperty.Name; - var perspexProperty = target.GetRegisteredProperties().First(property => property.Name == targetPropertyName); - - return new XamlBindingDefinition - ( - target, - perspexProperty, - new PropertyPath(Path), - Mode == BindingMode.Default ? BindingMode.OneWay : Mode - ); + return new XamlBindingDefinition(Path, Mode); } - /// The source path (for CLR bindings). public string Path { get; set; } - public BindingMode Mode { get; set; } } } \ No newline at end of file diff --git a/src/Markup/Perspex.Markup.Xaml/OmniXAML b/src/Markup/Perspex.Markup.Xaml/OmniXAML index 49e6ec001f..2a6309a4d9 160000 --- a/src/Markup/Perspex.Markup.Xaml/OmniXAML +++ b/src/Markup/Perspex.Markup.Xaml/OmniXAML @@ -1 +1 @@ -Subproject commit 49e6ec001f5873cf2290e0bc1f6f06ca9b9cf808 +Subproject commit 2a6309a4d9b60b848241a34bf2adfa16b52c7a85 diff --git a/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj b/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj index f6b9dcf204..987b630907 100644 --- a/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj +++ b/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj @@ -38,6 +38,10 @@ Properties\SharedAssemblyInfo.cs + + + + @@ -54,10 +58,6 @@ - - - - @@ -228,13 +228,6 @@ - - - - - - - @@ -244,8 +237,9 @@ - + + @@ -289,6 +283,10 @@ {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F} Perspex.Styling + + {6417e941-21bc-467b-a771-0de389353ce6} + Perspex.Markup + diff --git a/src/Markup/Perspex.Markup.Xaml/Templates/XamlDataTemplate.cs b/src/Markup/Perspex.Markup.Xaml/Templates/DataTemplate.cs similarity index 62% rename from src/Markup/Perspex.Markup.Xaml/Templates/XamlDataTemplate.cs rename to src/Markup/Perspex.Markup.Xaml/Templates/DataTemplate.cs index 49e5ce48a2..da22cafae2 100644 --- a/src/Markup/Perspex.Markup.Xaml/Templates/XamlDataTemplate.cs +++ b/src/Markup/Perspex.Markup.Xaml/Templates/DataTemplate.cs @@ -8,38 +8,26 @@ using Perspex.Controls.Templates; namespace Perspex.Markup.Xaml.Templates { - [ContentProperty("Content")] - public class XamlDataTemplate : IDataTemplate + public class DataTemplate : IDataTemplate { - private bool MyMatch(object data) + public Type DataType { get; set; } + public TemplateContent Content { get; set; } + + public bool Match(object data) { if (DataType == null) { - throw new InvalidOperationException("XAML DataTemplates must have a DataType"); + throw new InvalidOperationException("DataTemplate must have a DataType."); } return DataType == data.GetType(); } - private Control CreateVisualTreeForItem(object data) + public IControl Build(object data) { var visualTreeForItem = Content.Load(); visualTreeForItem.DataContext = data; return visualTreeForItem; } - - public Type DataType { get; set; } - - public TemplateContent Content { get; set; } - - public IControl Build(object param) - { - return CreateVisualTreeForItem(param); - } - - public bool Match(object data) - { - return MyMatch(data); - } } } \ No newline at end of file diff --git a/src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs new file mode 100644 index 0000000000..15cdf822bb --- /dev/null +++ b/src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs @@ -0,0 +1,53 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections; +using System.Reactive.Linq; +using Perspex.Controls; +using Perspex.Controls.Templates; +using Perspex.Markup.Binding; +using Perspex.Markup.Xaml.Binding; + +namespace Perspex.Markup.Xaml.Templates +{ + public class TreeDataTemplate : ITreeDataTemplate + { + public Type DataType { get; set; } + public TemplateContent Content { get; set; } + public XamlBindingDefinition ItemsSource { get; set; } + + public bool Match(object data) + { + if (DataType == null) + { + throw new InvalidOperationException("DataTemplate must have a DataType."); + } + + return DataType == data.GetType(); + } + + public IEnumerable ItemsSelector(object item) + { + if (ItemsSource != null) + { + var obs = new ExpressionObserver(item, ItemsSource.SourcePropertyPath); + return obs.Take(1).Wait() as IEnumerable; + } + + return null; + } + + public bool IsExpanded(object item) + { + return true; + } + + public IControl Build(object data) + { + var visualTreeForItem = Content.Load(); + visualTreeForItem.DataContext = data; + return visualTreeForItem; + } + } +} \ No newline at end of file diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs b/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs new file mode 100644 index 0000000000..d3520818dc --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs @@ -0,0 +1,102 @@ +// Copyright (c) The Perspex 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.Subjects; + +namespace Perspex.Markup.Binding +{ + internal abstract class ExpressionNode : IObservable + { + private object _target; + + private Subject _subject; + + private object _value = PerspexProperty.UnsetValue; + + public ExpressionNode Next { get; set; } + + public object Target + { + get { return _target; } + set + { + if (_target != null) + { + Unsubscribe(_target); + } + + _target = value; + + if (_target != null) + { + SubscribeAndUpdate(_target); + } + else + { + CurrentValue = PerspexProperty.UnsetValue; + } + + if (Next != null) + { + Next.Target = CurrentValue; + } + } + } + + public object CurrentValue + { + get + { + return _value; + } + + set + { + _value = value; + + if (Next != null) + { + Next.Target = value; + } + + if (_subject != null) + { + _subject.OnNext(value); + } + } + } + + public virtual bool SetValue(object value) + { + return Next?.SetValue(value) ?? false; + } + + public virtual IDisposable Subscribe(IObserver observer) + { + if (Next != null) + { + return Next.Subscribe(observer); + } + else + { + if (_subject == null) + { + _subject = new Subject(); + } + + observer.OnNext(CurrentValue); + return _subject.Subscribe(observer); + } + } + + protected virtual void SubscribeAndUpdate(object target) + { + CurrentValue = target; + } + + protected virtual void Unsubscribe(object target) + { + } + } +} diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionNodeBuilder.cs b/src/Markup/Perspex.Markup/Binding/ExpressionNodeBuilder.cs new file mode 100644 index 0000000000..953b1728b0 --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/ExpressionNodeBuilder.cs @@ -0,0 +1,29 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Perspex.Markup.Binding.Parsers; + +namespace Perspex.Markup.Binding +{ + internal static class ExpressionNodeBuilder + { + public static ExpressionNode Build(string expression) + { + if (string.IsNullOrWhiteSpace(expression)) + { + throw new ArgumentException("'expression' may not be empty."); + } + + var reader = new Reader(expression); + var node = ExpressionParser.Parse(reader); + + if (!reader.End) + { + throw new ExpressionParseException(reader, "Expected end of expression."); + } + + return node; + } + } +} diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs b/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs new file mode 100644 index 0000000000..2e76d58e24 --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs @@ -0,0 +1,115 @@ +// Copyright (c) The Perspex 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.Disposables; + +namespace Perspex.Markup.Binding +{ + /// + /// Observes and sets the value of an expression on an object. + /// + public class ExpressionObserver : ObservableBase, IDescription + { + private object _root; + private int _count; + private ExpressionNode _node; + + /// + /// Initializes a new instance of the class. + /// + /// The root object. + /// The expression. + public ExpressionObserver(object root, string expression) + { + _root = root; + _node = ExpressionNodeBuilder.Build(expression); + Expression = expression; + } + + /// + /// Attempts to set the value of a property expression. + /// + /// The value to set. + /// + /// True if the value could be set; false if the expression does not evaluate to a + /// property. + /// + public bool SetValue(object value) + { + IncrementCount(); + + try + { + return _node.SetValue(value); + } + finally + { + DecrementCount(); + } + } + + /// + /// Gets the expression being observed. + /// + public string Expression { get; } + + /// + /// Gets or sets the root object that the expression is being observed on. + /// + public object Root + { + get + { + return _root; + } + + set + { + if (_root != value) + { + _root = value; + + if (_count > 0) + { + _node.Target = _root; + } + } + } + } + + /// + string IDescription.Description => Expression; + + /// + protected override IDisposable SubscribeCore(IObserver observer) + { + IncrementCount(); + + var subscription = _node.Subscribe(observer); + + return Disposable.Create(() => + { + DecrementCount(); + subscription.Dispose(); + }); + } + + private void IncrementCount() + { + if (_count++ == 0) + { + _node.Target = Root; + } + } + + private void DecrementCount() + { + if (--_count == 0) + { + _node.Target = null; + } + } + } +} diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionParseException.cs b/src/Markup/Perspex.Markup/Binding/ExpressionParseException.cs new file mode 100644 index 0000000000..06505e1de8 --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/ExpressionParseException.cs @@ -0,0 +1,24 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Perspex.Markup.Binding.Parsers; + +namespace Perspex.Markup.Binding +{ + public class ExpressionParseException : Exception + { + internal ExpressionParseException(int column, string message) + : base(message) + { + Column = column; + } + + internal ExpressionParseException(Reader r, string message) + : this(r.Position, message) + { + } + + public int Column { get; } + } +} diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionSubject.cs b/src/Markup/Perspex.Markup/Binding/ExpressionSubject.cs new file mode 100644 index 0000000000..b8dd316b36 --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/ExpressionSubject.cs @@ -0,0 +1,51 @@ +// Copyright (c) The Perspex 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; + +namespace Perspex.Markup.Binding +{ + /// + /// Turns an into a subject that can be bound two-ways. + /// + public class ExpressionSubject : ISubject, IDescription + { + private ExpressionObserver _inner; + + /// + /// Initializes a new instance of the class. + /// + /// The . + public ExpressionSubject(ExpressionObserver inner) + { + _inner = inner; + } + + /// + string IDescription.Description => _inner.Expression; + + /// + public void OnCompleted() + { + } + + /// + public void OnError(Exception error) + { + } + + /// + public void OnNext(object value) + { + _inner.SetValue(value); + } + + /// + public IDisposable Subscribe(IObserver observer) + { + return _inner.Subscribe(observer); + } + } +} diff --git a/src/Markup/Perspex.Markup/Binding/IndexerNode.cs b/src/Markup/Perspex.Markup/Binding/IndexerNode.cs new file mode 100644 index 0000000000..5f1d840fbe --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/IndexerNode.cs @@ -0,0 +1,131 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Reflection; + +namespace Perspex.Markup.Binding +{ + internal class IndexerNode : ExpressionNode + { + private int[] _intArgs; + + public IndexerNode(IList arguments) + { + Arguments = arguments; + + var intArgs = Arguments.OfType().ToArray(); + + if (intArgs.Length == arguments.Count) + { + _intArgs = intArgs; + } + } + + public IList Arguments { get; } + + protected override void SubscribeAndUpdate(object target) + { + CurrentValue = GetValue(target); + + var incc = target as INotifyCollectionChanged; + + if (incc != null) + { + incc.CollectionChanged += CollectionChanged; + } + } + + protected override void Unsubscribe(object target) + { + var incc = target as INotifyCollectionChanged; + + if (incc != null) + { + incc.CollectionChanged -= CollectionChanged; + } + } + + private void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + bool update = false; + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + update = _intArgs[0] >= e.NewStartingIndex; + break; + case NotifyCollectionChangedAction.Remove: + update = _intArgs[0] >= e.OldStartingIndex; + break; + case NotifyCollectionChangedAction.Replace: + update = _intArgs[0] >= e.NewStartingIndex && + _intArgs[0] < e.NewStartingIndex + e.NewItems.Count; + break; + case NotifyCollectionChangedAction.Move: + update = (_intArgs[0] >= e.NewStartingIndex && + _intArgs[0] < e.NewStartingIndex + e.NewItems.Count) || + (_intArgs[0] >= e.OldStartingIndex && + _intArgs[0] < e.OldStartingIndex + e.OldItems.Count); + break; + case NotifyCollectionChangedAction.Reset: + update = true; + break; + } + + if (update) + { + CurrentValue = GetValue(sender); + } + } + + private object GetValue(object target) + { + var typeInfo = target.GetType().GetTypeInfo(); + var list = target as IList; + + if (typeInfo.IsArray && _intArgs != null) + { + var array = (Array)target; + + if (InBounds(_intArgs, array)) + { + return array.GetValue(_intArgs); + } + } + else if (target is IList && _intArgs?.Length == 1) + { + if (_intArgs[0] < list.Count) + { + return list[_intArgs[0]]; + } + } + + return PerspexProperty.UnsetValue; + } + + private bool InBounds(int[] args, Array array) + { + if (args.Length == array.Rank) + { + for (var i = 0; i < args.Length; ++i) + { + if (args[i] >= array.GetLength(i)) + { + return false; + } + } + + return true; + } + else + { + return false; + } + } + } +} diff --git a/src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs b/src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs new file mode 100644 index 0000000000..81ce06c301 --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs @@ -0,0 +1,40 @@ +// Copyright (c) The Perspex 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; + +namespace Perspex.Markup.Binding +{ + internal class LogicalNotNode : ExpressionNode + { + public override bool SetValue(object value) + { + throw new NotSupportedException("Cannot set a negated binding."); + } + + public override IDisposable Subscribe(IObserver observer) + { + return Next.Select(x => Negate(x)).Subscribe(observer); + } + + private object Negate(object v) + { + if (v != PerspexProperty.UnsetValue) + { + try + { + var boolean = Convert.ToBoolean(v, CultureInfo.InvariantCulture); + return !boolean; + } + catch + { + // TODO: Maybe should log something here. + } + } + + return PerspexProperty.UnsetValue; + } + } +} diff --git a/src/Markup/Perspex.Markup/Binding/Parsers/ArgumentListParser.cs b/src/Markup/Perspex.Markup/Binding/Parsers/ArgumentListParser.cs new file mode 100644 index 0000000000..bba9226a10 --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/Parsers/ArgumentListParser.cs @@ -0,0 +1,67 @@ +// Copyright (c) The Perspex 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; + +namespace Perspex.Markup.Binding.Parsers +{ + internal static class ArgumentListParser + { + public static IList Parse(Reader r, char open, char close) + { + if (r.Peek == open) + { + var result = new List(); + + r.Take(); + + while (!r.End) + { + var literal = LiteralParser.Parse(r); + + if (literal != null) + { + result.Add(literal); + } + else + { + throw new ExpressionParseException(r, "Expected integer."); + } + + r.SkipWhitespace(); + + if (r.End) + { + throw new ExpressionParseException(r, "Expected ','."); + } + else if (r.TakeIf(close)) + { + return result; + } + else + { + if (r.Take() != ',') + { + throw new ExpressionParseException(r, "Expected ','."); + } + + r.SkipWhitespace(); + } + } + + if (!r.End) + { + r.Take(); + return result; + } + else + { + throw new ExpressionParseException(r, "Expected ']'."); + } + } + + return null; + } + } +} diff --git a/src/Markup/Perspex.Markup/Binding/Parsers/ExpressionParser.cs b/src/Markup/Perspex.Markup/Binding/Parsers/ExpressionParser.cs new file mode 100644 index 0000000000..300585f730 --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/Parsers/ExpressionParser.cs @@ -0,0 +1,125 @@ +// Copyright (c) The Perspex 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 Perspex.Markup.Binding.Parsers +{ + internal static class ExpressionParser + { + public static ExpressionNode Parse(Reader r) + { + var nodes = new List(); + var state = State.Start; + + while (!r.End && state != State.End) + { + switch (state) + { + case State.Start: + state = ParseStart(r, nodes); + break; + + case State.AfterMember: + state = ParseAfterMember(r, nodes); + break; + + case State.BeforeMember: + state = ParseBeforeMember(r, nodes); + break; + } + } + + if (state == State.BeforeMember) + { + throw new ExpressionParseException(r, "Unexpected end of expression."); + } + + for (int n = 0; n < nodes.Count - 1; ++n) + { + nodes[n].Next = nodes[n + 1]; + } + + return nodes.FirstOrDefault(); + } + + private static State ParseStart(Reader r, IList nodes) + { + if (ParseNot(r)) + { + nodes.Add(new LogicalNotNode()); + return State.Start; + } + else + { + var identifier = IdentifierParser.Parse(r); + + if (identifier != null) + { + nodes.Add(new PropertyAccessorNode(identifier)); + return State.AfterMember; + } + } + + return State.End; + } + + private static State ParseAfterMember(Reader r, IList nodes) + { + if (ParseMemberAccessor(r)) + { + return State.BeforeMember; + } + else + { + var args = ArgumentListParser.Parse(r, '[', ']'); + + if (args != null) + { + if (args.Count == 0) + { + throw new ExpressionParseException(r, "Indexer may not be empty."); + } + + nodes.Add(new IndexerNode(args)); + return State.AfterMember; + } + } + + return State.End; + } + + private static State ParseBeforeMember(Reader r, IList nodes) + { + var identifier = IdentifierParser.Parse(r); + + if (identifier != null) + { + nodes.Add(new PropertyAccessorNode(identifier)); + return State.AfterMember; + } + + return State.End; + } + + private static bool ParseNot(Reader r) + { + return !r.End && r.TakeIf('!'); + } + + private static bool ParseMemberAccessor(Reader r) + { + return !r.End && r.TakeIf('.'); + } + + private enum State + { + Start, + AfterMember, + BeforeMember, + End, + } + } +} diff --git a/src/Markup/Perspex.Markup/Binding/Parsers/IdentifierParser.cs b/src/Markup/Perspex.Markup/Binding/Parsers/IdentifierParser.cs new file mode 100644 index 0000000000..6f3c760d74 --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/Parsers/IdentifierParser.cs @@ -0,0 +1,51 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Globalization; +using System.Text; + +namespace Perspex.Markup.Binding.Parsers +{ + internal static class IdentifierParser + { + public static string Parse(Reader r) + { + if (IsValidIdentifierStart(r.Peek)) + { + var result = new StringBuilder(); + + while (!r.End && IsValidIdentifierChar(r.Peek)) + { + result.Append(r.Take()); + } + + return result.ToString(); + } + else + { + return null; + } + } + + private static bool IsValidIdentifierStart(char c) + { + return char.IsLetter(c) || c == '_'; + } + + private static bool IsValidIdentifierChar(char c) + { + if (IsValidIdentifierStart(c)) + { + return true; + } + else + { + var cat = CharUnicodeInfo.GetUnicodeCategory(c); + return cat == UnicodeCategory.NonSpacingMark || + cat == UnicodeCategory.SpacingCombiningMark || + cat == UnicodeCategory.ConnectorPunctuation || + cat == UnicodeCategory.Format; + } + } + } +} diff --git a/src/Markup/Perspex.Markup/Binding/Parsers/LiteralParser.cs b/src/Markup/Perspex.Markup/Binding/Parsers/LiteralParser.cs new file mode 100644 index 0000000000..3026be551c --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/Parsers/LiteralParser.cs @@ -0,0 +1,35 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Globalization; +using System.Text; + +namespace Perspex.Markup.Binding.Parsers +{ + internal static class LiteralParser + { + public static object Parse(Reader r) + { + if (char.IsDigit(r.Peek)) + { + StringBuilder result = new StringBuilder(); + + while (!r.End) + { + if (char.IsDigit(r.Peek)) + { + result.Append(r.Take()); + } + else + { + break; + } + } + + return int.Parse(result.ToString(), CultureInfo.InvariantCulture); + } + + return null; + } + } +} diff --git a/src/Markup/Perspex.Markup/Binding/Parsers/Reader.cs b/src/Markup/Perspex.Markup/Binding/Parsers/Reader.cs new file mode 100644 index 0000000000..34c2746487 --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/Parsers/Reader.cs @@ -0,0 +1,44 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; + +namespace Perspex.Markup.Binding.Parsers +{ + internal class Reader + { + private string _s; + private int _i; + + public Reader(string s) + { + _s = s; + } + + public bool End => _i == _s.Length; + public char Peek => _s[_i]; + public int Position => _i; + public char Take() => _s[_i++]; + + public void SkipWhitespace() + { + while (!End && char.IsWhiteSpace(Peek)) + { + Take(); + } + } + + public bool TakeIf(char c) + { + if (Peek == c) + { + Take(); + return true; + } + else + { + return false; + } + } + } +} diff --git a/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs b/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs new file mode 100644 index 0000000000..1015bcfa0a --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs @@ -0,0 +1,164 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.ComponentModel; +using System.Reactive.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Input; + +namespace Perspex.Markup.Binding +{ + internal class PropertyAccessorNode : ExpressionNode + { + private PropertyInfo _propertyInfo; + private IDisposable _subscription; + + public PropertyAccessorNode(string propertyName) + { + PropertyName = propertyName; + } + + public string PropertyName { get; } + + public override bool SetValue(object value) + { + if (Next != null) + { + return Next.SetValue(value); + } + else + { + if (_propertyInfo != null && _propertyInfo.CanWrite) + { + _propertyInfo.SetValue(Target, value); + return true; + } + + return false; + } + } + + protected override void SubscribeAndUpdate(object target) + { + bool set = false; + + if (target != null) + { + _propertyInfo = FindProperty(target, PropertyName); + + if (_propertyInfo != null) + { + ReadValue(target); + set = true; + + var inpc = target as INotifyPropertyChanged; + + if (inpc != null) + { + inpc.PropertyChanged += PropertyChanged; + } + } + } + else + { + _propertyInfo = null; + } + + if (!set) + { + CurrentValue = PerspexProperty.UnsetValue; + } + } + + protected override void Unsubscribe(object target) + { + var inpc = target as INotifyPropertyChanged; + + if (inpc != null) + { + inpc.PropertyChanged -= PropertyChanged; + } + } + + private static PropertyInfo FindProperty(object target, string propertyName) + { + var typeInfo = target.GetType().GetTypeInfo(); + + do + { + var result = typeInfo.GetDeclaredProperty(propertyName); + + if (result != null) + { + return result; + } + else + { + typeInfo = typeInfo.BaseType?.GetTypeInfo(); + } + } while (typeInfo != null); + + return null; + } + + private void ReadValue(object target) + { + var value = _propertyInfo.GetValue(target); + var observable = value as IObservable; + var command = value as ICommand; + var task = value as Task; + bool set = false; + + // ReactiveCommand is an IObservable but we want to bind to it, not its value. + if (observable != null && command == null) + { + CurrentValue = PerspexProperty.UnsetValue; + set = true; + _subscription = observable + .ObserveOn(SynchronizationContext.Current) + .Subscribe(x => CurrentValue = x); + } + else if (task != null) + { + var resultProperty = task.GetType().GetTypeInfo().GetDeclaredProperty("Result"); + + if (resultProperty != null) + { + if (task.Status == TaskStatus.RanToCompletion) + { + CurrentValue = resultProperty.GetValue(task); + set = true; + } + else + { + task.ContinueWith( + x => CurrentValue = resultProperty.GetValue(task), + TaskScheduler.FromCurrentSynchronizationContext()) + .ConfigureAwait(false); + } + } + } + else + { + CurrentValue = value; + set = true; + } + + if (!set) + { + CurrentValue = PerspexProperty.UnsetValue; + } + } + + private void PropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == PropertyName) + { + ReadValue(sender); + } + } + } +} diff --git a/src/Markup/Perspex.Markup/Perspex.Markup.csproj b/src/Markup/Perspex.Markup/Perspex.Markup.csproj new file mode 100644 index 0000000000..e16d0f1deb --- /dev/null +++ b/src/Markup/Perspex.Markup/Perspex.Markup.csproj @@ -0,0 +1,89 @@ + + + + + 11.0 + Debug + AnyCPU + {6417E941-21BC-467B-A771-0DE389353CE6} + Library + Properties + Perspex.Markup + Perspex.Markup + en-US + 512 + {786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Profile7 + v4.5 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + bin\Debug\Perspex.Markup.XML + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + bin\Release\Perspex.Markup.XML + + + + + + + + + + + + + + + + + + + + ..\..\..\packages\Rx-Core.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Core.dll + True + + + ..\..\..\packages\Rx-Interfaces.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Interfaces.dll + True + + + ..\..\..\packages\Rx-Linq.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Linq.dll + True + + + ..\..\..\packages\Rx-PlatformServices.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.PlatformServices.dll + True + + + + + + + + {b09b78d8-9b26-48b0-9149-d64a2f120f3f} + Perspex.Base + + + + + \ No newline at end of file diff --git a/src/Markup/Perspex.Markup/Properties/AssemblyInfo.cs b/src/Markup/Perspex.Markup/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..9076f81754 --- /dev/null +++ b/src/Markup/Perspex.Markup/Properties/AssemblyInfo.cs @@ -0,0 +1,32 @@ +using System.Resources; +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("Perspex.Markup")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Perspex.Markup")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: NeutralResourcesLanguage("en")] + +// 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")] + +[assembly: InternalsVisibleTo("Perspex.Markup.UnitTests")] \ No newline at end of file diff --git a/src/Markup/Perspex.Markup/packages.config b/src/Markup/Perspex.Markup/packages.config new file mode 100644 index 0000000000..5bc4ae393b --- /dev/null +++ b/src/Markup/Perspex.Markup/packages.config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/Perspex.Base/BindingDescriptor.cs b/src/Perspex.Base/BindingDescriptor.cs index 1f491ec029..0006843ae3 100644 --- a/src/Perspex.Base/BindingDescriptor.cs +++ b/src/Perspex.Base/BindingDescriptor.cs @@ -27,7 +27,7 @@ namespace Perspex TwoWay, /// - /// Copies the target to the source one time and then disposes of the binding. + /// Updates the target when the application starts or when the data context changes. /// OneTime, diff --git a/src/Perspex.Base/IObservablePropertyBag.cs b/src/Perspex.Base/IObservablePropertyBag.cs index 3bc8e41178..451cb63a89 100644 --- a/src/Perspex.Base/IObservablePropertyBag.cs +++ b/src/Perspex.Base/IObservablePropertyBag.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Reactive.Subjects; namespace Perspex { @@ -39,6 +40,43 @@ namespace Perspex IObservable source, BindingPriority priority = BindingPriority.LocalValue); + /// + /// Initiates a two-way binding between s. + /// + /// The property on this object. + /// The source object. + /// The property on the source object. + /// The priority of the binding. + /// + /// A disposable which can be used to terminate the binding. + /// + /// + /// The binding is first carried out from to this. + /// + IDisposable BindTwoWay( + PerspexProperty property, + PerspexObject source, + PerspexProperty sourceProperty, + BindingPriority priority = BindingPriority.LocalValue); + + /// + /// Initiates a two-way binding between a and an + /// . + /// + /// The property on this object. + /// The subject to bind to. + /// The priority of the binding. + /// + /// A disposable which can be used to terminate the binding. + /// + /// + /// The binding is first carried out from to this. + /// + IDisposable BindTwoWay( + PerspexProperty property, + ISubject source, + BindingPriority priority = BindingPriority.LocalValue); + /// /// Gets an observable for a . /// diff --git a/src/Perspex.Base/IPropertyBag.cs b/src/Perspex.Base/IPropertyBag.cs index 68b1b6fed8..fe29c349ef 100644 --- a/src/Perspex.Base/IPropertyBag.cs +++ b/src/Perspex.Base/IPropertyBag.cs @@ -8,6 +8,11 @@ namespace Perspex /// public interface IPropertyBag { + /// + /// Gets the object that inherited values are inherited from. + /// + IPropertyBag InheritanceParent { get; } + /// /// Clears a 's local value. /// diff --git a/src/Perspex.Base/PerspexObject.cs b/src/Perspex.Base/PerspexObject.cs index 7a0e211f2c..696d01893a 100644 --- a/src/Perspex.Base/PerspexObject.cs +++ b/src/Perspex.Base/PerspexObject.cs @@ -7,6 +7,7 @@ using System.ComponentModel; using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; +using System.Reactive.Subjects; using System.Reflection; using Perspex.Reactive; using Perspex.Utilities; @@ -99,6 +100,11 @@ namespace Perspex remove { _inpcChanged -= value; } } + /// + /// Gets the object that inherited values are inherited from. + /// + IPropertyBag IPropertyBag.InheritanceParent => InheritanceParent; + /// /// Gets or sets the parent object that inherited values /// are inherited from. @@ -186,7 +192,7 @@ namespace Perspex if (sourceBinding == null && mode > BindingMode.OneWay) { - throw new InvalidOperationException("Can only bind OneWay to plain IObservable."); + mode = BindingMode.OneWay; } switch (mode) @@ -334,7 +340,7 @@ namespace Perspex PropertyChanged -= handler; }); }, - GetObservableDescription(property)); + GetDescription(property)); } /// @@ -377,7 +383,7 @@ namespace Perspex PropertyChanged -= handler; }); }, - GetObservableDescription(property)); + GetDescription(property)); } /// @@ -456,8 +462,15 @@ namespace Perspex public bool IsSet(PerspexProperty property) { Contract.Requires(property != null); + + PriorityValue value; + + if (_values.TryGetValue(property, out value)) + { + return value.Value != PerspexProperty.UnsetValue; + } - return _values.ContainsKey(property); + return false; } /// @@ -492,6 +505,7 @@ namespace Perspex throw new ArgumentException($"The property {property.Name} is readonly."); } + LogPropertySet(property, value, priority); property.Setter(this, value); } else @@ -524,14 +538,9 @@ namespace Perspex _values.Add(property, v); } + LogPropertySet(property, value, priority); v.SetValue(value, (int)priority); } - - _propertyLog.Verbose( - "Set {Property} to {$Value} with priority {Priority}", - property, - value, - priority); } /// @@ -557,6 +566,7 @@ namespace Perspex throw new ArgumentException($"The property {property.Name} is readonly."); } + LogPropertySet(property, value, priority); property.Setter(this, value); } else @@ -593,14 +603,15 @@ namespace Perspex _propertyLog.Verbose( "Bound {Property} to {Binding} with priority LocalValue", property, - source); + GetDescription(source)); - return source.Subscribe(x => SetValue(property, x)); + return source + .Select(x => TypeUtilities.CastOrDefault(x, property.PropertyType, false)) + .Subscribe(x => SetValue(property, x)); } else { PriorityValue v; - IDescription description = source as IDescription; if (!IsRegistered(property)) { @@ -616,7 +627,7 @@ namespace Perspex _propertyLog.Verbose( "Bound {Property} to {Binding} with priority {Priority}", property, - source, + GetDescription(source), priority); return v.Add(source, (int)priority); @@ -658,7 +669,7 @@ namespace Perspex } /// - /// Initialites a two-way bind between s. + /// Initiates a two-way binding between s. /// /// The property on this object. /// The source object. @@ -676,11 +687,46 @@ namespace Perspex PerspexProperty sourceProperty, BindingPriority priority = BindingPriority.LocalValue) { + _propertyLog.Verbose( + "Bound two way {Property} to {Binding} with priority {Priority}", + property, + source, + priority); + return new CompositeDisposable( Bind(property, source.GetObservable(sourceProperty)), source.Bind(sourceProperty, GetObservable(property))); } + /// + /// Initiates a two-way binding between a and an + /// . + /// + /// The property on this object. + /// The subject to bind to. + /// The priority of the binding. + /// + /// A disposable which can be used to terminate the binding. + /// + /// + /// The binding is first carried out from to this. + /// + public IDisposable BindTwoWay( + PerspexProperty property, + ISubject source, + BindingPriority priority = BindingPriority.LocalValue) + { + _propertyLog.Verbose( + "Bound two way {Property} to {Binding} with priority {Priority}", + property, + GetDescription(source), + priority); + + return new CompositeDisposable( + Bind(property, source), + GetObservable(property).Subscribe(source)); + } + /// /// Forces the specified property to be revalidated. /// @@ -926,11 +972,37 @@ namespace Perspex /// /// The property /// The description. - private string GetObservableDescription(PerspexProperty property) + private string GetDescription(PerspexProperty property) { return string.Format("{0}.{1}", GetType().Name, property.Name); } + /// + /// Gets a description of an observable that van be used in logs. + /// + /// The observable. + /// The description. + private string GetDescription(IObservable o) + { + var description = o as IDescription; + return description?.Description ?? o.ToString(); + } + + /// + /// Logs a property set message. + /// + /// The property. + /// The new value. + /// The priority. + private void LogPropertySet(PerspexProperty property, object value, BindingPriority priority) + { + _propertyLog.Verbose( + "Set {Property} to {$Value} with priority {Priority}", + property, + value, + priority); + } + /// /// Throws an exception indicating that the specified property is not registered on this /// object. diff --git a/src/Perspex.Base/PriorityValue.cs b/src/Perspex.Base/PriorityValue.cs index 53ecd8da99..011f07cfa0 100644 --- a/src/Perspex.Base/PriorityValue.cs +++ b/src/Perspex.Base/PriorityValue.cs @@ -212,25 +212,23 @@ namespace Perspex /// The priority level that the value came from. private void UpdateValue(object value, int priority) { - if (!TypeUtilities.TryCast(_valueType, value, out value)) + if (TypeUtilities.TryCast(_valueType, value, out value)) { - throw new InvalidOperationException(string.Format( - "Invalid value for Property '{0}': {1} ({2})", - _name, - value, - value?.GetType().FullName ?? "(null)")); - } + var old = _value; - var old = _value; + if (_validate != null) + { + value = _validate(value); + } - if (_validate != null) + ValuePriority = priority; + _value = value; + _changed.OnNext(Tuple.Create(old, _value)); + } + else { - value = _validate(value); + // TODO: Log error. } - - ValuePriority = priority; - _value = value; - _changed.OnNext(Tuple.Create(old, _value)); } /// diff --git a/src/Perspex.Base/Utilities/TypeUtilities.cs b/src/Perspex.Base/Utilities/TypeUtilities.cs index 98909d9801..d026efc622 100644 --- a/src/Perspex.Base/Utilities/TypeUtilities.cs +++ b/src/Perspex.Base/Utilities/TypeUtilities.cs @@ -32,8 +32,9 @@ namespace Perspex.Utilities /// The type to cast to. /// The value to cast. /// If sucessful, contains the cast value. + /// Allow . /// True if the cast was sucessful, otherwise false. - public static bool TryCast(Type to, object value, out object result) + public static bool TryCast(Type to, object value, out object result, bool allowUnset = true) { Contract.Requires(to != null); @@ -46,7 +47,7 @@ namespace Perspex.Utilities var from = value.GetType(); - if (value == PerspexProperty.UnsetValue) + if (allowUnset && value == PerspexProperty.UnsetValue) { result = value; return true; @@ -77,5 +78,32 @@ namespace Perspex.Utilities result = null; return false; } + + /// + /// Casts a value to a type, returning the default for that type if the value could not be + /// cast. + /// + /// The value to cast. + /// The type to cast to.. + /// Allow . + /// A value of . + public static object CastOrDefault(object value, Type type, bool allowUnset = true) + { + var typeInfo = type.GetTypeInfo(); + object result; + + if (TypeUtilities.TryCast(type, value, out result, allowUnset)) + { + return result; + } + else if (typeInfo.IsValueType) + { + return Activator.CreateInstance(type); + } + else + { + return null; + } + } } } diff --git a/src/Perspex.Controls/Border.cs b/src/Perspex.Controls/Border.cs index 8a70987cb0..bcb1235f25 100644 --- a/src/Perspex.Controls/Border.cs +++ b/src/Perspex.Controls/Border.cs @@ -83,7 +83,7 @@ namespace Perspex.Controls /// Renders the control. /// /// The drawing context. - public override void Render(IDrawingContext context) + public override void Render(DrawingContext context) { var background = Background; var borderBrush = BorderBrush; diff --git a/src/Perspex.Controls/ContentControl.cs b/src/Perspex.Controls/ContentControl.cs index 4ee47a8ef9..6297ec4f71 100644 --- a/src/Perspex.Controls/ContentControl.cs +++ b/src/Perspex.Controls/ContentControl.cs @@ -11,7 +11,7 @@ using Perspex.Layout; namespace Perspex.Controls { /// - /// Displays according to a . + /// Displays according to a . /// public class ContentControl : TemplatedControl, IContentControl, IReparentingHost { diff --git a/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs b/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs index 1a0fdb1a5f..d4aad6236e 100644 --- a/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs @@ -16,7 +16,7 @@ namespace Perspex.Controls.Generators /// The type of the container. public class TreeItemContainerGenerator : ITreeItemContainerGenerator where T : TreeViewItem, new() { - private readonly Dictionary _containers = new Dictionary(); + private Dictionary _containers = new Dictionary(); private readonly Subject _containersInitialized = new Subject(); @@ -102,7 +102,9 @@ namespace Perspex.Controls.Generators /// The removed controls. public IList ClearContainers() { - throw new NotImplementedException(); + var result = _containers; + _containers = new Dictionary(); + return result.Values.Cast().ToList(); } /// @@ -192,14 +194,14 @@ namespace Perspex.Controls.Generators if (template == null) { - template = DataTemplate.Default; + template = FuncDataTemplate.Default; } var treeTemplate = template as ITreeDataTemplate; if (treeTemplate == null) { - treeTemplate = new TreeDataTemplate(typeof(object), template.Build, x => null); + treeTemplate = new FuncTreeDataTemplate(typeof(object), template.Build, x => null); } return treeTemplate; diff --git a/src/Perspex.Controls/IContentControl.cs b/src/Perspex.Controls/IContentControl.cs index cc2c4a3fea..50978a79cb 100644 --- a/src/Perspex.Controls/IContentControl.cs +++ b/src/Perspex.Controls/IContentControl.cs @@ -7,7 +7,7 @@ namespace Perspex.Controls { /// /// Defines a control that displays according to a - /// . + /// . /// public interface IContentControl : IControl { diff --git a/src/Perspex.Controls/Image.cs b/src/Perspex.Controls/Image.cs index 79b79c6141..0b316689bd 100644 --- a/src/Perspex.Controls/Image.cs +++ b/src/Perspex.Controls/Image.cs @@ -46,7 +46,7 @@ namespace Perspex.Controls /// Renders the control. /// /// The drawing context. - public override void Render(IDrawingContext context) + public override void Render(DrawingContext context) { Bitmap source = Source; diff --git a/src/Perspex.Controls/ItemsControl.cs b/src/Perspex.Controls/ItemsControl.cs index 6649b43cb0..adcd1e0768 100644 --- a/src/Perspex.Controls/ItemsControl.cs +++ b/src/Perspex.Controls/ItemsControl.cs @@ -156,8 +156,6 @@ namespace Perspex.Controls /// The event args. protected virtual void ItemsChanged(PerspexPropertyChangedEventArgs e) { - System.Diagnostics.Debug.WriteLine($"{this.GetType().Name} set items"); - var incc = e.OldValue as INotifyCollectionChanged; if (incc != null) diff --git a/src/Perspex.Controls/Panel.cs b/src/Perspex.Controls/Panel.cs index 08e6621adb..bd45bf1e3a 100644 --- a/src/Perspex.Controls/Panel.cs +++ b/src/Perspex.Controls/Panel.cs @@ -172,10 +172,10 @@ namespace Perspex.Controls } /// - /// Renders the visual to a . + /// Renders the visual to a . /// /// The drawing context. - public override void Render(IDrawingContext context) + public override void Render(DrawingContext context) { Brush background = Background; if (background != null) diff --git a/src/Perspex.Controls/Perspex.Controls.csproj b/src/Perspex.Controls/Perspex.Controls.csproj index f29b366032..3cec434017 100644 --- a/src/Perspex.Controls/Perspex.Controls.csproj +++ b/src/Perspex.Controls/Perspex.Controls.csproj @@ -72,7 +72,7 @@ - + @@ -102,7 +102,7 @@ - + @@ -129,7 +129,7 @@ - + @@ -157,7 +157,7 @@ - + diff --git a/src/Perspex.Controls/Presenters/TextPresenter.cs b/src/Perspex.Controls/Presenters/TextPresenter.cs index 1e6068fd7d..2948cd541a 100644 --- a/src/Perspex.Controls/Presenters/TextPresenter.cs +++ b/src/Perspex.Controls/Presenters/TextPresenter.cs @@ -69,7 +69,7 @@ namespace Perspex.Controls.Presenters return hit.TextPosition + (hit.IsTrailing ? 1 : 0); } - public override void Render(IDrawingContext context) + public override void Render(DrawingContext context) { var selectionStart = SelectionStart; var selectionEnd = SelectionEnd; diff --git a/src/Perspex.Controls/Primitives/AccessText.cs b/src/Perspex.Controls/Primitives/AccessText.cs index 33da32037f..0168974248 100644 --- a/src/Perspex.Controls/Primitives/AccessText.cs +++ b/src/Perspex.Controls/Primitives/AccessText.cs @@ -62,7 +62,7 @@ namespace Perspex.Controls.Primitives /// Renders the to a drawing context. /// /// The drawing context. - public override void Render(IDrawingContext context) + public override void Render(DrawingContext context) { base.Render(context); diff --git a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs index 139b861dc1..2a3841d08a 100644 --- a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs @@ -295,8 +295,6 @@ namespace Perspex.Controls.Primitives /// The event args. private void SelectedIndexChanged(PerspexPropertyChangedEventArgs e) { - System.Diagnostics.Debug.WriteLine($"{this.GetType().Name} set selected index"); - var index = (int)e.OldValue; if (index != -1) diff --git a/src/Perspex.Controls/Properties/AssemblyInfo.cs b/src/Perspex.Controls/Properties/AssemblyInfo.cs index bf1ddab5d3..7c0a189889 100644 --- a/src/Perspex.Controls/Properties/AssemblyInfo.cs +++ b/src/Perspex.Controls/Properties/AssemblyInfo.cs @@ -9,5 +9,6 @@ using Perspex.Metadata; [assembly: InternalsVisibleTo("Perspex.Controls.UnitTests")] [assembly: XmlnsDefinition("https://github.com/perspex", "Perspex.Controls")] +[assembly: XmlnsDefinition("https://github.com/perspex", "Perspex.Controls.Presenters")] [assembly: XmlnsDefinition("https://github.com/perspex", "Perspex.Controls.Primitives")] [assembly: XmlnsDefinition("https://github.com/perspex", "Perspex.Controls.Shapes")] \ No newline at end of file diff --git a/src/Perspex.Controls/Shapes/Shape.cs b/src/Perspex.Controls/Shapes/Shape.cs index 5f5b989602..58dc24a738 100644 --- a/src/Perspex.Controls/Shapes/Shape.cs +++ b/src/Perspex.Controls/Shapes/Shape.cs @@ -90,7 +90,7 @@ namespace Perspex.Controls.Shapes set { SetValue(StrokeThicknessProperty, value); } } - public override void Render(IDrawingContext context) + public override void Render(DrawingContext context) { var geometry = RenderedGeometry; diff --git a/src/Perspex.Controls/Templates/DataTemplateExtensions.cs b/src/Perspex.Controls/Templates/DataTemplateExtensions.cs index 8e885ede2d..fbd4086ed5 100644 --- a/src/Perspex.Controls/Templates/DataTemplateExtensions.cs +++ b/src/Perspex.Controls/Templates/DataTemplateExtensions.cs @@ -42,7 +42,7 @@ namespace Perspex.Controls.Templates } else { - result = DataTemplate.Default.Build(data); + result = FuncDataTemplate.Default.Build(data); } result.DataContext = data; diff --git a/src/Perspex.Controls/Templates/DataTemplate.cs b/src/Perspex.Controls/Templates/FuncDataTemplate.cs similarity index 80% rename from src/Perspex.Controls/Templates/DataTemplate.cs rename to src/Perspex.Controls/Templates/FuncDataTemplate.cs index 2c18cfdb13..ab2e203d4c 100644 --- a/src/Perspex.Controls/Templates/DataTemplate.cs +++ b/src/Perspex.Controls/Templates/FuncDataTemplate.cs @@ -9,13 +9,13 @@ namespace Perspex.Controls.Templates /// /// Builds a control for a piece of data. /// - public class DataTemplate : FuncTemplate, IDataTemplate + public class FuncDataTemplate : FuncTemplate, IDataTemplate { /// /// The default data template used in the case where not matching data template is found. /// - public static readonly DataTemplate Default = - new DataTemplate(typeof(object), o => (o != null) ? new TextBlock { Text = o.ToString() } : null); + public static readonly FuncDataTemplate Default = + new FuncDataTemplate(typeof(object), o => (o != null) ? new TextBlock { Text = o.ToString() } : null); /// /// The implementation of the method. @@ -23,19 +23,19 @@ namespace Perspex.Controls.Templates private readonly Func _match; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The type of data which the data template matches. /// /// A function which when passed an object of returns a control. /// - public DataTemplate(Type type, Func build) + public FuncDataTemplate(Type type, Func build) : this(o => IsInstance(o, type), build) { } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// /// A function which determines whether the data template matches the specified data. @@ -43,7 +43,7 @@ namespace Perspex.Controls.Templates /// /// A function which returns a control for matching data. /// - public DataTemplate(Func match, Func build) + public FuncDataTemplate(Func match, Func build) : base(build) { Contract.Requires(match != null); diff --git a/src/Perspex.Controls/Templates/DataTemplate`1.cs b/src/Perspex.Controls/Templates/FuncDataTemplate`1.cs similarity index 84% rename from src/Perspex.Controls/Templates/DataTemplate`1.cs rename to src/Perspex.Controls/Templates/FuncDataTemplate`1.cs index ba8a56b8d1..b0021d093d 100644 --- a/src/Perspex.Controls/Templates/DataTemplate`1.cs +++ b/src/Perspex.Controls/Templates/FuncDataTemplate`1.cs @@ -9,21 +9,21 @@ namespace Perspex.Controls.Templates /// Builds a control for a piece of data of specified type. /// /// The type of the template's data. - public class DataTemplate : DataTemplate + public class FuncDataTemplate : FuncDataTemplate { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// /// A function which when passed an object of returns a control. /// - public DataTemplate(Func build) + public FuncDataTemplate(Func build) : base(typeof(T), CastBuild(build)) { } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// /// A function which determines whether the data template matches the specified data. @@ -31,7 +31,7 @@ namespace Perspex.Controls.Templates /// /// A function which when passed an object of returns a control. /// - public DataTemplate(Func match, Func build) + public FuncDataTemplate(Func match, Func build) : base(CastMatch(match), CastBuild(build)) { } diff --git a/src/Perspex.Controls/Templates/TreeDataTemplate.cs b/src/Perspex.Controls/Templates/FuncTreeDataTemplate.cs similarity index 89% rename from src/Perspex.Controls/Templates/TreeDataTemplate.cs rename to src/Perspex.Controls/Templates/FuncTreeDataTemplate.cs index e384aaf90d..0442f9a649 100644 --- a/src/Perspex.Controls/Templates/TreeDataTemplate.cs +++ b/src/Perspex.Controls/Templates/FuncTreeDataTemplate.cs @@ -10,14 +10,14 @@ namespace Perspex.Controls.Templates /// /// A template used to build hierachical data. /// - public class TreeDataTemplate : DataTemplate, ITreeDataTemplate + public class FuncTreeDataTemplate : FuncDataTemplate, ITreeDataTemplate { private readonly Func _itemsSelector; private readonly Func _isExpanded; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The type of data which the data template matches. /// @@ -27,7 +27,7 @@ namespace Perspex.Controls.Templates /// A function which when passed an object of returns the child /// items. /// - public TreeDataTemplate( + public FuncTreeDataTemplate( Type type, Func build, Func itemsSelector) @@ -36,7 +36,7 @@ namespace Perspex.Controls.Templates } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The type of data which the data template matches. /// @@ -50,7 +50,7 @@ namespace Perspex.Controls.Templates /// A function which when passed an object of returns the the /// initial expanded state of the node. /// - public TreeDataTemplate( + public FuncTreeDataTemplate( Type type, Func build, Func itemsSelector, @@ -60,7 +60,7 @@ namespace Perspex.Controls.Templates } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// /// A function which determines whether the data template matches the specified data. @@ -71,7 +71,7 @@ namespace Perspex.Controls.Templates /// /// A function which when passed a matching object returns the child items. /// - public TreeDataTemplate( + public FuncTreeDataTemplate( Func match, Func build, Func itemsSelector) @@ -81,7 +81,7 @@ namespace Perspex.Controls.Templates } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// /// A function which determines whether the data template matches the specified data. @@ -96,7 +96,7 @@ namespace Perspex.Controls.Templates /// A function which when passed a matching object returns the the initial expanded state /// of the node. /// - public TreeDataTemplate( + public FuncTreeDataTemplate( Func match, Func build, Func itemsSelector, diff --git a/src/Perspex.Controls/Templates/TreeDataTemplate`1.cs b/src/Perspex.Controls/Templates/FuncTreeDataTemplate`1.cs similarity index 88% rename from src/Perspex.Controls/Templates/TreeDataTemplate`1.cs rename to src/Perspex.Controls/Templates/FuncTreeDataTemplate`1.cs index 68b3ebbf6d..18de28feb6 100644 --- a/src/Perspex.Controls/Templates/TreeDataTemplate`1.cs +++ b/src/Perspex.Controls/Templates/FuncTreeDataTemplate`1.cs @@ -10,10 +10,10 @@ namespace Perspex.Controls.Templates /// A template used to build hierachical data. /// /// The type of the template's data. - public class TreeDataTemplate : TreeDataTemplate + public class FuncTreeDataTemplate : FuncTreeDataTemplate { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// /// A function which when passed an object of returns a control. @@ -22,7 +22,7 @@ namespace Perspex.Controls.Templates /// A function which when passed an object of returns the child /// items. /// - public TreeDataTemplate( + public FuncTreeDataTemplate( Func build, Func itemsSelector) : base( @@ -33,7 +33,7 @@ namespace Perspex.Controls.Templates } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// /// A function which when passed an object of returns a control. @@ -46,7 +46,7 @@ namespace Perspex.Controls.Templates /// A function which when passed an object of returns the the /// initial expanded state of the node. /// - public TreeDataTemplate( + public FuncTreeDataTemplate( Func build, Func itemsSelector, Func isExpanded) @@ -59,7 +59,7 @@ namespace Perspex.Controls.Templates } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// /// A function which determines whether the data template matches the specified data. @@ -70,7 +70,7 @@ namespace Perspex.Controls.Templates /// /// A function which when passed a matching object returns the child items. /// - public TreeDataTemplate( + public FuncTreeDataTemplate( Func match, Func build, Func itemsSelector) @@ -82,7 +82,7 @@ namespace Perspex.Controls.Templates } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// /// A function which determines whether the data template matches the specified data. @@ -97,7 +97,7 @@ namespace Perspex.Controls.Templates /// A function which when passed a matching object returns the the initial expanded state /// of the node. /// - public TreeDataTemplate( + public FuncTreeDataTemplate( Func match, Func build, Func itemsSelector, diff --git a/src/Perspex.Controls/TextBlock.cs b/src/Perspex.Controls/TextBlock.cs index cf407e3f41..b6bec05240 100644 --- a/src/Perspex.Controls/TextBlock.cs +++ b/src/Perspex.Controls/TextBlock.cs @@ -65,7 +65,7 @@ namespace Perspex.Controls /// Defines the property. /// public static readonly PerspexProperty TextProperty = - PerspexProperty.Register(nameof(Text)); + PerspexProperty.Register(nameof(Text), defaultBindingMode: BindingMode.TwoWay); /// /// Defines the property. @@ -214,7 +214,7 @@ namespace Perspex.Controls /// Renders the to a drawing context. /// /// The drawing context. - public override void Render(IDrawingContext context) + public override void Render(DrawingContext context) { Brush background = Background; diff --git a/src/Perspex.Diagnostics/Views/LogicalTreeView.cs b/src/Perspex.Diagnostics/Views/LogicalTreeView.cs index d68bc71fee..276506bf09 100644 --- a/src/Perspex.Diagnostics/Views/LogicalTreeView.cs +++ b/src/Perspex.Diagnostics/Views/LogicalTreeView.cs @@ -48,7 +48,7 @@ namespace Perspex.Diagnostics.Views { DataTemplates = new DataTemplates { - new TreeDataTemplate(GetHeader, x => x.Children), + new FuncTreeDataTemplate(GetHeader, x => x.Children), }, [!ItemsControl.ItemsProperty] = this.WhenAnyValue(x => x.ViewModel.Nodes), }), diff --git a/src/Perspex.Diagnostics/Views/VisualTreeView.cs b/src/Perspex.Diagnostics/Views/VisualTreeView.cs index 485ed264aa..df7277e396 100644 --- a/src/Perspex.Diagnostics/Views/VisualTreeView.cs +++ b/src/Perspex.Diagnostics/Views/VisualTreeView.cs @@ -49,7 +49,7 @@ namespace Perspex.Diagnostics.Views { DataTemplates = new DataTemplates { - new TreeDataTemplate(GetHeader, x => x.Children), + new FuncTreeDataTemplate(GetHeader, x => x.Children), }, [!ItemsControl.ItemsProperty] = this.WhenAnyValue(x => x.ViewModel.Nodes), }), diff --git a/src/Perspex.HtmlRenderer/Adapters/GraphicsAdapter.cs b/src/Perspex.HtmlRenderer/Adapters/GraphicsAdapter.cs index 8b52a1b573..6fd918a69b 100644 --- a/src/Perspex.HtmlRenderer/Adapters/GraphicsAdapter.cs +++ b/src/Perspex.HtmlRenderer/Adapters/GraphicsAdapter.cs @@ -32,7 +32,7 @@ namespace TheArtOfDev.HtmlRenderer.Perspex.Adapters /// /// The wrapped Perspex graphics object /// - private readonly IDrawingContext _g; + private readonly DrawingContext _g; /// /// if to release the graphics object on dispose @@ -51,7 +51,7 @@ namespace TheArtOfDev.HtmlRenderer.Perspex.Adapters /// the Perspex graphics object to use /// the initial clip of the graphics /// optional: if to release the graphics object on dispose (default - false) - public GraphicsAdapter(IDrawingContext g, RRect initialClip, bool releaseGraphics = false) + public GraphicsAdapter(DrawingContext g, RRect initialClip, bool releaseGraphics = false) : base(PerspexAdapter.Instance, initialClip) { ArgChecker.AssertArgNotNull(g, "g"); diff --git a/src/Perspex.HtmlRenderer/HtmlContainer.cs b/src/Perspex.HtmlRenderer/HtmlContainer.cs index 9ada42d5ed..e30036b343 100644 --- a/src/Perspex.HtmlRenderer/HtmlContainer.cs +++ b/src/Perspex.HtmlRenderer/HtmlContainer.cs @@ -360,7 +360,7 @@ namespace TheArtOfDev.HtmlRenderer.Perspex /// /// the device to use to render /// the clip rectangle of the html container - public void PerformPaint(IDrawingContext g, Rect clip) + public void PerformPaint(DrawingContext g, Rect clip) { ArgChecker.AssertArgNotNull(g, "g"); diff --git a/src/Perspex.HtmlRenderer/HtmlControl.cs b/src/Perspex.HtmlRenderer/HtmlControl.cs index b720b17bc0..ddd7541923 100644 --- a/src/Perspex.HtmlRenderer/HtmlControl.cs +++ b/src/Perspex.HtmlRenderer/HtmlControl.cs @@ -327,7 +327,7 @@ namespace Perspex.Controls.Html private Size RenderSize => new Size(Bounds.Width, Bounds.Height); - public override void Render(IDrawingContext context) + public override void Render(DrawingContext context) { context.FillRectangle(Background, new Rect(RenderSize)); diff --git a/src/Perspex.Input/IInputElement.cs b/src/Perspex.Input/IInputElement.cs index d6e5378f02..e8658c99b0 100644 --- a/src/Perspex.Input/IInputElement.cs +++ b/src/Perspex.Input/IInputElement.cs @@ -114,14 +114,8 @@ namespace Perspex.Input void Focus(); /// - /// Returns the input element that can be found within the current control at the specified - /// position. + /// Gets the key bindings for the element. /// - /// The position, in control coordinates. - /// The at the specified position. - IInputElement InputHitTest(Point p); - - List KeyBindings { get; } } } diff --git a/src/Perspex.Input/InputElement.cs b/src/Perspex.Input/InputElement.cs index 43cfc840c6..7e0907c9e8 100644 --- a/src/Perspex.Input/InputElement.cs +++ b/src/Perspex.Input/InputElement.cs @@ -339,17 +339,6 @@ namespace Perspex.Input public List KeyBindings { get; } = new List(); - /// - /// Returns the input element that can be found within the current control at the specified - /// position. - /// - /// The position, in control coordinates. - /// The at the specified position. - public IInputElement InputHitTest(Point p) - { - return this.GetInputElementsAt(p).FirstOrDefault(); - } - /// /// Focuses the control. /// diff --git a/src/Perspex.Input/InputExtensions.cs b/src/Perspex.Input/InputExtensions.cs index ef3f848073..50d826b2a9 100644 --- a/src/Perspex.Input/InputExtensions.cs +++ b/src/Perspex.Input/InputExtensions.cs @@ -7,8 +7,19 @@ using System.Linq; namespace Perspex.Input { + /// + /// Defines extensions for the interface. + /// public static class InputExtensions { + /// + /// Returns the active input elements at a point on an . + /// + /// The element to test. + /// The point on . + /// + /// The active input elements found at the point, ordered topmost first. + /// public static IEnumerable GetInputElementsAt(this IInputElement element, Point p) { Contract.Requires(element != null); @@ -22,7 +33,7 @@ namespace Perspex.Input if (element.VisualChildren.Any()) { - foreach (var child in element.VisualChildren.OfType()) + foreach (var child in ZSort(element.VisualChildren.OfType())) { foreach (var result in child.GetInputElementsAt(p)) { @@ -34,5 +45,51 @@ namespace Perspex.Input yield return element; } } + + /// + /// Returns the topmost active input element at a point on an . + /// + /// The element to test. + /// The point on . + /// The topmost at the specified position. + public static IInputElement InputHitTest(this IInputElement element, Point p) + { + return element.GetInputElementsAt(p).First(); + } + + private static IEnumerable ZSort(IEnumerable elements) + { + return elements + .Select((element, index) => new ZOrderElement + { + Element = element, + Index = index, + ZIndex = element.ZIndex, + }) + .OrderBy(x => x, null) + .Select(x => x.Element); + + } + + private class ZOrderElement : IComparable + { + public IInputElement Element { get; set; } + public int Index { get; set; } + public int ZIndex { get; set; } + + public int CompareTo(ZOrderElement other) + { + var z = other.ZIndex - ZIndex; + + if (z != 0) + { + return z; + } + else + { + return other.Index - Index; + } + } + } } } diff --git a/src/Perspex.SceneGraph/IVisual.cs b/src/Perspex.SceneGraph/IVisual.cs index 7126558483..0491d63fad 100644 --- a/src/Perspex.SceneGraph/IVisual.cs +++ b/src/Perspex.SceneGraph/IVisual.cs @@ -76,10 +76,10 @@ namespace Perspex int ZIndex { get; set; } /// - /// Renders the scene graph node to a . + /// Renders the scene graph node to a . /// /// The context. - void Render(IDrawingContext context); + void Render(DrawingContext context); /// /// Returns a transform that transforms the visual's coordinates into the coordinates diff --git a/src/Perspex.SceneGraph/Media/Color.cs b/src/Perspex.SceneGraph/Media/Color.cs index 9a474ef210..35856ddb63 100644 --- a/src/Perspex.SceneGraph/Media/Color.cs +++ b/src/Perspex.SceneGraph/Media/Color.cs @@ -106,7 +106,7 @@ namespace Perspex.Media throw new FormatException($"Invalid color string: '{s}'."); } - return FromUInt32(uint.Parse(s.Substring(1), NumberStyles.HexNumber) | or); + return FromUInt32(uint.Parse(s.Substring(1), NumberStyles.HexNumber, CultureInfo.InvariantCulture) | or); } else { diff --git a/src/Perspex.SceneGraph/Media/DrawingContext.cs b/src/Perspex.SceneGraph/Media/DrawingContext.cs new file mode 100644 index 0000000000..a191cb1fce --- /dev/null +++ b/src/Perspex.SceneGraph/Media/DrawingContext.cs @@ -0,0 +1,206 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Perspex.Media.Imaging; + +namespace Perspex.Media +{ + public sealed class DrawingContext : IDisposable + { + private readonly IDrawingContextImpl _impl; + private int _currentLevel; + + private Stack _transformContainers = new Stack(); + + struct TransformContainer + { + public Matrix LocalTransform; + public Matrix ContainerTransform; + + public TransformContainer(Matrix localTransform, Matrix containerTransform) + { + LocalTransform = localTransform; + ContainerTransform = containerTransform; + } + } + + public DrawingContext(IDrawingContextImpl impl) + { + _impl = impl; + } + + + private Matrix _currentTransform = Matrix.Identity; + + /// + /// Gets the current transform of the drawing context. + /// + public Matrix CurrentTransform + { + get { return _currentTransform; } + private set + { + _currentTransform = value; + _impl.Transform = _currentTransform*_currentContainerTransform; + } + } + + private Matrix _currentContainerTransform = Matrix.Identity; + + /// + /// Draws a bitmap image. + /// + /// The bitmap image. + /// The opacity to draw with. + /// The rect in the image to draw. + /// The rect in the output to draw to. + public void DrawImage(IBitmap source, double opacity, Rect sourceRect, Rect destRect) + => _impl.DrawImage(source, opacity, sourceRect, destRect); + + /// + /// Draws a line. + /// + /// The stroke pen. + /// The first point of the line. + /// The second point of the line. + public void DrawLine(Pen pen, Point p1, Point p2) => _impl.DrawLine(pen, p1, p2); + + /// + /// Draws a geometry. + /// + /// The fill brush. + /// The stroke pen. + /// The geometry. + public void DrawGeometry(Brush brush, Pen pen, Geometry geometry) => _impl.DrawGeometry(brush, pen, geometry); + + /// + /// Draws the outline of a rectangle. + /// + /// The pen. + /// The rectangle bounds. + /// The corner radius. + public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0.0f) + => _impl.DrawRectangle(pen, rect, cornerRadius); + + /// + /// Draws text. + /// + /// The foreground brush. + /// The upper-left corner of the text. + /// The text. + public void DrawText(Brush foreground, Point origin, FormattedText text) + => _impl.DrawText(foreground, origin, text); + + /// + /// Draws a filled rectangle. + /// + /// The brush. + /// The rectangle bounds. + /// The corner radius. + public void FillRectangle(Brush brush, Rect rect, float cornerRadius = 0.0f) + => _impl.FillRectangle(brush, rect, cornerRadius); + + public struct PushedState : IDisposable + { + private readonly int _level; + private readonly DrawingContext _context; + private readonly Matrix _matrix; + private readonly PushedStateType _type; + + public enum PushedStateType + { + None, + Matrix, + Opacity, + Clip, + MatrixContainer + } + + public PushedState(DrawingContext context, PushedStateType type, Matrix matrix = default(Matrix)) + { + _level = context._currentLevel += 1; + _context = context; + _type = type; + _matrix = matrix; + + } + + public void Dispose() + { + if(_type == PushedStateType.None) + return; + if (_context._currentLevel != _level) + throw new InvalidOperationException("Wrong Push/Pop state order"); + _context._currentLevel--; + if (_type == PushedStateType.Matrix) + _context.CurrentTransform = _matrix; + else if(_type == PushedStateType.Clip) + _context._impl.PopClip(); + else if(_type == PushedStateType.Opacity) + _context._impl.PopOpacity(); + else if (_type == PushedStateType.MatrixContainer) + { + var cont = _context._transformContainers.Pop(); + _context._currentContainerTransform = cont.ContainerTransform; + _context.CurrentTransform = cont.LocalTransform; + } + } + } + + + /// + /// Pushes a clip rectange. + /// + /// The clip rectangle. + /// A disposable used to undo the clip rectangle. + public PushedState PushClip(Rect clip) + { + _impl.PushClip(clip); + return new PushedState(this, PushedState.PushedStateType.Clip); + } + + /// + /// Pushes an opacity value. + /// + /// The opacity. + /// A disposable used to undo the opacity. + public PushedState PushOpacity(double opacity) + //TODO: Elimintate platform-specific push opacity call + { + _impl.PushOpacity(opacity); + return new PushedState(this, PushedState.PushedStateType.Opacity); + } + + /// + /// Pushes a matrix transformation. + /// + /// The matrix + /// A disposable used to undo the transformation. + public PushedState PushPostTransform(Matrix matrix) => PushSetTransform(CurrentTransform*matrix); + + public PushedState PushPreTransform(Matrix matrix) => PushSetTransform(matrix*CurrentTransform); + + + PushedState PushSetTransform(Matrix matrix) + { + var oldMatrix = CurrentTransform; + CurrentTransform = matrix; + + return new PushedState(this, PushedState.PushedStateType.Matrix, oldMatrix); + } + + + public PushedState PushTransformContainer() + { + _transformContainers.Push(new TransformContainer(CurrentTransform, _currentContainerTransform)); + _currentContainerTransform = CurrentTransform*_currentContainerTransform; + _currentTransform = Matrix.Identity; + return new PushedState(this, PushedState.PushedStateType.MatrixContainer); + } + + public void Dispose() => _impl.Dispose(); + } +} diff --git a/src/Perspex.SceneGraph/Media/IDrawingContext.cs b/src/Perspex.SceneGraph/Media/IDrawingContext.cs index 92d1fc4e98..4d144b33e0 100644 --- a/src/Perspex.SceneGraph/Media/IDrawingContext.cs +++ b/src/Perspex.SceneGraph/Media/IDrawingContext.cs @@ -9,12 +9,12 @@ namespace Perspex.Media /// /// Defines the interface through which drawing occurs. /// - public interface IDrawingContext : IDisposable + public interface IDrawingContextImpl : IDisposable { /// - /// Gets the current transform of the drawing context. + /// Gets or sets the current transform of the drawing context. /// - Matrix CurrentTransform { get; } + Matrix Transform { get; set; } /// /// Draws a bitmap image. @@ -70,20 +70,17 @@ namespace Perspex.Media /// /// The clip rectangle. /// A disposable used to undo the clip rectangle. - IDisposable PushClip(Rect clip); + void PushClip(Rect clip); + + void PopClip(); /// /// Pushes an opacity value. /// /// The opacity. /// A disposable used to undo the opacity. - IDisposable PushOpacity(double opacity); + void PushOpacity(double opacity); - /// - /// Pushes a matrix transformation. - /// - /// The matrix - /// A disposable used to undo the transformation. - IDisposable PushTransform(Matrix matrix); + void PopOpacity(); } } diff --git a/src/Perspex.SceneGraph/Media/Imaging/RenderTargetBitmap.cs b/src/Perspex.SceneGraph/Media/Imaging/RenderTargetBitmap.cs index 5b8bbea6da..20fc55ebd8 100644 --- a/src/Perspex.SceneGraph/Media/Imaging/RenderTargetBitmap.cs +++ b/src/Perspex.SceneGraph/Media/Imaging/RenderTargetBitmap.cs @@ -47,7 +47,7 @@ namespace Perspex.Media.Imaging return factory.CreateRenderTargetBitmap(width, height); } - public IDrawingContext CreateDrawingContext() => PlatformImpl.CreateDrawingContext(); + public DrawingContext CreateDrawingContext() => PlatformImpl.CreateDrawingContext(); void IRenderTarget.Resize(int width, int height) { diff --git a/src/Perspex.SceneGraph/Media/ValidatingDrawingContext.cs b/src/Perspex.SceneGraph/Media/ValidatingDrawingContext.cs deleted file mode 100644 index 86ae342894..0000000000 --- a/src/Perspex.SceneGraph/Media/ValidatingDrawingContext.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Reactive.Disposables; -using Perspex.Media.Imaging; - -namespace Perspex.Media -{ - public class ValidatingDrawingContext : IDrawingContext - { - private readonly IDrawingContext _base; - - public ValidatingDrawingContext(IDrawingContext @base) - { - _base = @base; - } - - public void Dispose() - { - _base.Dispose(); - } - - public Matrix CurrentTransform => _base.CurrentTransform; - public void DrawImage(IBitmap source, double opacity, Rect sourceRect, Rect destRect) - { - _base.DrawImage(source, opacity, sourceRect, destRect); - } - - public void DrawLine(Pen pen, Point p1, Point p2) - { - _base.DrawLine(pen, p1, p2); - } - - public void DrawGeometry(Brush brush, Pen pen, Geometry geometry) - { - _base.DrawGeometry(brush, pen, geometry); - } - - public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0) - { - _base.DrawRectangle(pen, rect, cornerRadius); - } - - public void DrawText(Brush foreground, Point origin, FormattedText text) - { - _base.DrawText(foreground, origin, text); - } - - public void FillRectangle(Brush brush, Rect rect, float cornerRadius = 0) - { - _base.FillRectangle(brush, rect, cornerRadius); - } - - - Stack _stateStack = new Stack(); - - IDisposable Transform(IDisposable disposable) - { - _stateStack.Push(disposable); - return Disposable.Create(() => - { - var current = _stateStack.Peek(); - if (current != disposable) - throw new InvalidOperationException("Invalid push/pop order"); - current.Dispose(); - _stateStack.Pop(); - }); - } - - public IDisposable PushClip(Rect clip) => Transform(_base.PushClip(clip)); - - public IDisposable PushOpacity(double opacity) => Transform(_base.PushOpacity(opacity)); - - public IDisposable PushTransform(Matrix matrix) => Transform(_base.PushTransform(matrix)); - } -} \ No newline at end of file diff --git a/src/Perspex.SceneGraph/Perspex.SceneGraph.csproj b/src/Perspex.SceneGraph/Perspex.SceneGraph.csproj index e09bed7d90..d2d897142f 100644 --- a/src/Perspex.SceneGraph/Perspex.SceneGraph.csproj +++ b/src/Perspex.SceneGraph/Perspex.SceneGraph.csproj @@ -64,6 +64,7 @@ + @@ -99,7 +100,6 @@ - diff --git a/src/Perspex.SceneGraph/Platform/IRenderTarget.cs b/src/Perspex.SceneGraph/Platform/IRenderTarget.cs index fdbf391a05..a5c8f71d79 100644 --- a/src/Perspex.SceneGraph/Platform/IRenderTarget.cs +++ b/src/Perspex.SceneGraph/Platform/IRenderTarget.cs @@ -15,9 +15,9 @@ namespace Perspex.Platform public interface IRenderTarget : IDisposable { /// - /// Creates an for a rendering session. + /// Creates an for a rendering session. /// - IDrawingContext CreateDrawingContext(); + DrawingContext CreateDrawingContext(); /// /// Resizes the rendered viewport. diff --git a/src/Perspex.SceneGraph/Rendering/RendererBase.cs b/src/Perspex.SceneGraph/Rendering/RendererBase.cs index ba4a0d35c1..7a7db8f842 100644 --- a/src/Perspex.SceneGraph/Rendering/RendererBase.cs +++ b/src/Perspex.SceneGraph/Rendering/RendererBase.cs @@ -35,7 +35,7 @@ namespace Perspex.Rendering /// The visual to render. /// /// The drawing context. - public static void Render(this IDrawingContext context, IVisual visual) + public static void Render(this DrawingContext context, IVisual visual) { var opacity = visual.Opacity; if (visual.IsVisible && opacity > 0) @@ -50,11 +50,12 @@ namespace Perspex.Rendering var offset = Matrix.CreateTranslation(origin); renderTransform = (-offset)*visual.RenderTransform.Value*(offset); } - m = context.CurrentTransform.Invert()*renderTransform*m*context.CurrentTransform; + m = renderTransform*m; - using (context.PushTransform(m)) + using (context.PushPostTransform(m)) using (context.PushOpacity(opacity)) - using (visual.ClipToBounds ? context.PushClip(new Rect(visual.Bounds.Size)) : null) + using (visual.ClipToBounds ? context.PushClip(new Rect(visual.Bounds.Size)) : default(DrawingContext.PushedState)) + using (context.PushTransformContainer()) { visual.Render(context); foreach (var child in visual.VisualChildren.OrderBy(x => x.ZIndex)) diff --git a/src/Perspex.SceneGraph/Visual.cs b/src/Perspex.SceneGraph/Visual.cs index e835c6a76c..84940468ea 100644 --- a/src/Perspex.SceneGraph/Visual.cs +++ b/src/Perspex.SceneGraph/Visual.cs @@ -192,6 +192,11 @@ namespace Perspex /// /// Gets the Z index of the node. /// + /// + /// Controls with a higher will appear in front of controls with + /// a lower ZIndex. If two controls have the same ZIndex then the control that appears + /// later in the containing element's children collection will appear on top. + /// public int ZIndex { get { return GetValue(ZIndexProperty); } @@ -229,10 +234,10 @@ namespace Perspex } /// - /// Renders the visual to a . + /// Renders the visual to a . /// /// The drawing context. - public virtual void Render(IDrawingContext context) + public virtual void Render(DrawingContext context) { Contract.Requires(context != null); } diff --git a/src/Perspex.Themes.Default/MenuItemStyle.cs b/src/Perspex.Themes.Default/MenuItemStyle.cs index a53b0e865c..3ff1638dc6 100644 --- a/src/Perspex.Themes.Default/MenuItemStyle.cs +++ b/src/Perspex.Themes.Default/MenuItemStyle.cs @@ -21,8 +21,8 @@ namespace Perspex.Themes.Default /// public class MenuItemStyle : Styles { - private static readonly DataTemplate AccessKeyDataTemplate = - new DataTemplate(x => new AccessText { Text = x }); + private static readonly FuncDataTemplate AccessKeyDataTemplate = + new FuncDataTemplate(x => new AccessText { Text = x }); /// /// Initializes a new instance of the class. diff --git a/src/Windows/Perspex.Direct2D1/Media/DrawingContext.cs b/src/Windows/Perspex.Direct2D1/Media/DrawingContext.cs index 0b6abe9353..b10208d7e0 100644 --- a/src/Windows/Perspex.Direct2D1/Media/DrawingContext.cs +++ b/src/Windows/Perspex.Direct2D1/Media/DrawingContext.cs @@ -2,6 +2,8 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections; +using System.Collections.Generic; using System.Reactive.Disposables; using Perspex.Media; using SharpDX; @@ -13,7 +15,7 @@ namespace Perspex.Direct2D1.Media /// /// Draws using Direct2D1. /// - public class DrawingContext : IDrawingContext, IDisposable + public class DrawingContext : IDrawingContextImpl, IDisposable { /// /// The Direct2D1 render target. @@ -42,10 +44,10 @@ namespace Perspex.Direct2D1.Media /// /// Gets the current transform of the drawing context. /// - public Matrix CurrentTransform + public Matrix Transform { get { return _renderTarget.Transform.ToPerspex(); } - private set { _renderTarget.Transform = value.ToDirect2D(); } + set { _renderTarget.Transform = value.ToDirect2D(); } } /// @@ -53,6 +55,8 @@ namespace Perspex.Direct2D1.Media /// public void Dispose() { + foreach (var layer in _layerPool) + layer.Dispose(); _renderTarget.EndDraw(); } @@ -233,22 +237,24 @@ namespace Perspex.Direct2D1.Media /// /// The clip rectangle. /// A disposable used to undo the clip rectangle. - public IDisposable PushClip(Rect clip) + public void PushClip(Rect clip) { _renderTarget.PushAxisAlignedClip(clip.ToSharpDX(), AntialiasMode.PerPrimitive); + } - return Disposable.Create(() => - { - _renderTarget.PopAxisAlignedClip(); - }); + public void PopClip() + { + _renderTarget.PopAxisAlignedClip(); } + Stack _layers = new Stack(); + private readonly Stack _layerPool = new Stack(); /// /// Pushes an opacity value. /// /// The opacity. /// A disposable used to undo the opacity. - public IDisposable PushOpacity(double opacity) + public void PushOpacity(double opacity) { if (opacity < 1) { @@ -256,41 +262,26 @@ namespace Perspex.Direct2D1.Media { ContentBounds = RectangleF.Infinite, MaskTransform = Matrix3x2.Identity, - Opacity = (float)opacity, + Opacity = (float) opacity, }; - var layer = new Layer(_renderTarget); - + var layer = _layerPool.Count != 0 ? _layerPool.Pop() : new Layer(_renderTarget); _renderTarget.PushLayer(ref parameters, layer); - return Disposable.Create(() => - { - _renderTarget.PopLayer(); - layer.Dispose(); - }); + _layers.Push(layer); } else - { - return Disposable.Empty; - } + _layers.Push(null); } - /// - /// Pushes a matrix transformation. - /// - /// The matrix - /// A disposable used to undo the transformation. - public IDisposable PushTransform(Matrix matrix) + public void PopOpacity() { - Matrix3x2 m3x2 = matrix.ToDirect2D(); - Matrix3x2 transform = _renderTarget.Transform * m3x2; - _renderTarget.Transform = transform; - - return Disposable.Create(() => + var layer = _layers.Pop(); + if (layer != null) { - m3x2.Invert(); - _renderTarget.Transform = transform * m3x2; - }); + _renderTarget.PopLayer(); + _layerPool.Push(layer); + } } /// diff --git a/src/Windows/Perspex.Direct2D1/Media/Imaging/RenderTargetBitmapImpl.cs b/src/Windows/Perspex.Direct2D1/Media/Imaging/RenderTargetBitmapImpl.cs index 97efa00d47..cf785aa444 100644 --- a/src/Windows/Perspex.Direct2D1/Media/Imaging/RenderTargetBitmapImpl.cs +++ b/src/Windows/Perspex.Direct2D1/Media/Imaging/RenderTargetBitmapImpl.cs @@ -38,7 +38,7 @@ namespace Perspex.Direct2D1.Media // TODO: } - public IDrawingContext CreateDrawingContext() => new RenderTarget(_target).CreateDrawingContext(); + public Perspex.Media.DrawingContext CreateDrawingContext() => new RenderTarget(_target).CreateDrawingContext(); void IRenderTarget.Resize(int width, int height) { diff --git a/src/Windows/Perspex.Direct2D1/Media/VisualBrushImpl.cs b/src/Windows/Perspex.Direct2D1/Media/VisualBrushImpl.cs index ddb82fba19..74377b67e6 100644 --- a/src/Windows/Perspex.Direct2D1/Media/VisualBrushImpl.cs +++ b/src/Windows/Perspex.Direct2D1/Media/VisualBrushImpl.cs @@ -55,7 +55,7 @@ namespace Perspex.Direct2D1.Media using (var ctx = renderer.CreateDrawingContext()) using (ctx.PushClip(drawRect)) - using (ctx.PushTransform(transform)) + using (ctx.PushPostTransform(transform)) { ctx.Render(visual); } diff --git a/src/Windows/Perspex.Direct2D1/RenderTarget.cs b/src/Windows/Perspex.Direct2D1/RenderTarget.cs index 5feabdde87..66ffe80448 100644 --- a/src/Windows/Perspex.Direct2D1/RenderTarget.cs +++ b/src/Windows/Perspex.Direct2D1/RenderTarget.cs @@ -8,6 +8,7 @@ using Perspex.Platform; using Perspex.Rendering; using SharpDX; using SharpDX.Direct2D1; +using DrawingContext = Perspex.Media.DrawingContext; using DwFactory = SharpDX.DirectWrite.Factory; namespace Perspex.Direct2D1 @@ -91,24 +92,12 @@ namespace Perspex.Direct2D1 window.Resize(new Size2(width, height)); } - IDrawingContext Wrap(IDrawingContext ctx) - { -#if DEBUG - return new ValidatingDrawingContext(ctx); -#endif -#pragma warning disable 162 - return ctx; -#pragma warning restore 162 - } - /// /// Creates a drawing context for a rendering session. /// - /// An . - public IDrawingContext CreateDrawingContext() - { - return Wrap(new DrawingContext(_renderTarget, DirectWriteFactory)); - } + /// An . + public DrawingContext CreateDrawingContext() + => new DrawingContext(new Media.DrawingContext(_renderTarget, DirectWriteFactory)); public void Dispose() { diff --git a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs index 7c8e60543d..49534082a9 100644 --- a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs +++ b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs @@ -59,14 +59,11 @@ namespace Perspex.Base.UnitTests } [Fact] - public void Bind_Throws_Exception_For_Invalid_Value_Type() + public void Bind_Ignores_Invalid_Value_Type() { Class1 target = new Class1(); - - Assert.Throws(() => - { - target.Bind((PerspexProperty)Class1.FooProperty, Observable.Return((object)123)); - }); + target.Bind((PerspexProperty)Class1.FooProperty, Observable.Return((object)123)); + Assert.Equal("foodefault", target.GetValue(Class1.FooProperty)); } [Fact] diff --git a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs index c8b4470d13..a9a2d5c1fc 100644 --- a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs +++ b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs @@ -148,6 +148,45 @@ namespace Perspex.Base.UnitTests Assert.Equal("second", target.Foo); } + [Fact] + public void Bind_Handles_Wrong_Type() + { + var target = new Class1(); + var source = new Subject(); + + var sub = target.Bind(Class1.FooProperty, source); + + source.OnNext(45); + + Assert.Equal(null, target.Foo); + } + + [Fact] + public void Bind_Handles_Wrong_Value_Type() + { + var target = new Class1(); + var source = new Subject(); + + var sub = target.Bind(Class1.BazProperty, source); + + source.OnNext("foo"); + + Assert.Equal(0, target.Baz); + } + + [Fact] + public void Bind_Handles_UnsetValue() + { + var target = new Class1(); + var source = new Subject(); + + var sub = target.Bind(Class1.BazProperty, source); + + source.OnNext(PerspexProperty.UnsetValue); + + Assert.Equal(0, target.Baz); + } + [Fact] public void ReadOnly_Property_Cannot_Be_Set() { @@ -295,9 +334,12 @@ namespace Perspex.Base.UnitTests public static readonly PerspexProperty BarProperty = PerspexProperty.RegisterDirect("Bar", o => o.Bar); - private string _foo = "initial"; + public static readonly PerspexProperty BazProperty = + PerspexProperty.RegisterDirect("Bar", o => o.Baz, (o,v) => o.Baz = v); + private string _foo = "initial"; private string _bar = "bar"; + private int _baz = 5; public string Foo { @@ -309,6 +351,12 @@ namespace Perspex.Base.UnitTests { get { return _bar; } } + + public int Baz + { + get { return _baz; } + set { SetAndRaise(BazProperty, ref _baz, value); } + } } private class Class2 : PerspexObject diff --git a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Metadata.cs b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Metadata.cs index b905f8b5ba..7fa816cf82 100644 --- a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Metadata.cs +++ b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Metadata.cs @@ -42,6 +42,35 @@ namespace Perspex.Base.UnitTests Assert.Equal(new[] { "Attached" }, names); } + [Fact] + public void IsSet_Returns_False_For_Unset_Property() + { + var target = new Class1(); + + Assert.False(target.IsSet(Class1.FooProperty)); + } + + [Fact] + public void IsSet_Returns_False_For_Set_Property() + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "foo"); + + Assert.True(target.IsSet(Class1.FooProperty)); + } + + [Fact] + public void IsSet_Returns_False_For_Cleared_Property() + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "foo"); + target.SetValue(Class1.FooProperty, PerspexProperty.UnsetValue); + + Assert.False(target.IsSet(Class1.FooProperty)); + } + private class Class1 : PerspexObject { public static readonly PerspexProperty FooProperty = diff --git a/tests/Perspex.Controls.UnitTests/ItemsControlTests.cs b/tests/Perspex.Controls.UnitTests/ItemsControlTests.cs index da7d4b953f..124a559ef0 100644 --- a/tests/Perspex.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Perspex.Controls.UnitTests/ItemsControlTests.cs @@ -305,7 +305,7 @@ namespace Perspex.Controls.UnitTests DataContext = "Base", DataTemplates = new DataTemplates { - new DataTemplate(x => new Button { Content = x }) + new FuncDataTemplate(x => new Button { Content = x }) }, Items = items, }; diff --git a/tests/Perspex.Controls.UnitTests/TabControlTests.cs b/tests/Perspex.Controls.UnitTests/TabControlTests.cs index 927db0a76e..3bce41a515 100644 --- a/tests/Perspex.Controls.UnitTests/TabControlTests.cs +++ b/tests/Perspex.Controls.UnitTests/TabControlTests.cs @@ -148,7 +148,7 @@ namespace Perspex.Controls.UnitTests DataContext = "Base", DataTemplates = new DataTemplates { - new DataTemplate(x => new Button { Content = x }) + new FuncDataTemplate(x => new Button { Content = x }) }, Items = items, }; diff --git a/tests/Perspex.Input.UnitTests/InputElement_HitTesting.cs b/tests/Perspex.Input.UnitTests/InputElement_HitTesting.cs new file mode 100644 index 0000000000..aeb11aa413 --- /dev/null +++ b/tests/Perspex.Input.UnitTests/InputElement_HitTesting.cs @@ -0,0 +1,129 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Perspex.Controls; +using Perspex.Layout; +using Xunit; + +namespace Perspex.Input.UnitTests +{ + public class InputElement_HitTesting + { + [Fact] + public void InputHitTest_Should_Find_Control_At_Point() + { + var container = new Decorator + { + Width = 200, + Height = 200, + Child = new Border + { + Width = 100, + Height = 100, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + } + }; + + container.Measure(Size.Infinity); + container.Arrange(new Rect(container.DesiredSize)); + + var result = container.InputHitTest(new Point(100, 100)); + + Assert.Equal(container.Child, result); + } + + [Fact] + public void InputHitTest_Should_Not_Find_Control_Outside_Point() + { + var container = new Decorator + { + Width = 200, + Height = 200, + Child = new Border + { + Width = 100, + Height = 100, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + } + }; + + container.Measure(Size.Infinity); + container.Arrange(new Rect(container.DesiredSize)); + + var result = container.InputHitTest(new Point(10, 10)); + + Assert.Equal(container, result); + } + + [Fact] + public void InputHitTest_Should_Find_Top_Control_At_Point() + { + var container = new Panel + { + Width = 200, + Height = 200, + Children = new Controls.Controls + { + new Border + { + Width = 100, + Height = 100, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }, + new Border + { + Width = 50, + Height = 50, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + } + } + }; + + container.Measure(Size.Infinity); + container.Arrange(new Rect(container.DesiredSize)); + + var result = container.InputHitTest(new Point(100, 100)); + + Assert.Equal(container.Children[1], result); + } + + [Fact] + public void InputHitTest_Should_Find_Top_Control_At_Point_With_ZOrder() + { + var container = new Panel + { + Width = 200, + Height = 200, + Children = new Controls.Controls + { + new Border + { + Width = 100, + Height = 100, + ZIndex = 1, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }, + new Border + { + Width = 50, + Height = 50, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + } + } + }; + + container.Measure(Size.Infinity); + container.Arrange(new Rect(container.DesiredSize)); + + var result = container.InputHitTest(new Point(100, 100)); + + Assert.Equal(container.Children[0], result); + } + } +} diff --git a/tests/Perspex.Input.UnitTests/Perspex.Input.UnitTests.csproj b/tests/Perspex.Input.UnitTests/Perspex.Input.UnitTests.csproj index 1aff689782..051e2a966f 100644 --- a/tests/Perspex.Input.UnitTests/Perspex.Input.UnitTests.csproj +++ b/tests/Perspex.Input.UnitTests/Perspex.Input.UnitTests.csproj @@ -55,6 +55,7 @@ + diff --git a/tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests.cs b/tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests.cs new file mode 100644 index 0000000000..424586cf45 --- /dev/null +++ b/tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests.cs @@ -0,0 +1,148 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Collections.Generic; +using System.Linq; +using Perspex.Markup.Binding; +using Xunit; + +namespace Perspex.Markup.UnitTests.Binding +{ + public class ExpressionNodeBuilderTests + { + [Fact] + public void Should_Build_Single_Property() + { + var result = ToList(ExpressionNodeBuilder.Build("Foo")); + + AssertIsProperty(result[0], "Foo"); + } + + [Fact] + public void Should_Build_Underscored_Property() + { + var result = ToList(ExpressionNodeBuilder.Build("_Foo")); + + AssertIsProperty(result[0], "_Foo"); + } + + [Fact] + public void Should_Build_Property_Chain() + { + var result = ToList(ExpressionNodeBuilder.Build("Foo.Bar.Baz")); + + Assert.Equal(3, result.Count); + AssertIsProperty(result[0], "Foo"); + AssertIsProperty(result[1], "Bar"); + AssertIsProperty(result[2], "Baz"); + } + + [Fact] + public void Should_Build_Negated_Property_Chain() + { + var result = ToList(ExpressionNodeBuilder.Build("!Foo.Bar.Baz")); + + Assert.Equal(4, result.Count); + Assert.IsType(result[0]); + AssertIsProperty(result[1], "Foo"); + AssertIsProperty(result[2], "Bar"); + AssertIsProperty(result[3], "Baz"); + } + + [Fact] + public void Should_Build_Double_Negated_Property_Chain() + { + var result = ToList(ExpressionNodeBuilder.Build("!!Foo.Bar.Baz")); + + Assert.Equal(5, result.Count); + Assert.IsType(result[0]); + Assert.IsType(result[1]); + AssertIsProperty(result[2], "Foo"); + AssertIsProperty(result[3], "Bar"); + AssertIsProperty(result[4], "Baz"); + } + + [Fact] + public void Should_Build_Indexed_Property() + { + var result = ToList(ExpressionNodeBuilder.Build("Foo[15]")); + + Assert.Equal(2, result.Count); + AssertIsProperty(result[0], "Foo"); + AssertIsIndexer(result[1], 15); + Assert.IsType(result[1]); + } + + [Fact] + public void Should_Build_Multiple_Indexed_Property() + { + var result = ToList(ExpressionNodeBuilder.Build("Foo[15,6]")); + + Assert.Equal(2, result.Count); + AssertIsProperty(result[0], "Foo"); + AssertIsIndexer(result[1], 15, 6); + } + + [Fact] + public void Should_Build_Multiple_Indexed_Property_With_Space() + { + var result = ToList(ExpressionNodeBuilder.Build("Foo[5, 16]")); + + Assert.Equal(2, result.Count); + AssertIsProperty(result[0], "Foo"); + AssertIsIndexer(result[1], 5, 16); + } + + [Fact] + public void Should_Build_Consecutive_Indexers() + { + var result = ToList(ExpressionNodeBuilder.Build("Foo[15][16]")); + + Assert.Equal(3, result.Count); + AssertIsProperty(result[0], "Foo"); + AssertIsIndexer(result[1], 15); + AssertIsIndexer(result[2], 16); + } + + [Fact] + public void Should_Build_Indexed_Property_In_Chain() + { + var result = ToList(ExpressionNodeBuilder.Build("Foo.Bar[5, 6].Baz")); + + Assert.Equal(4, result.Count); + AssertIsProperty(result[0], "Foo"); + AssertIsProperty(result[1], "Bar"); + AssertIsIndexer(result[2], 5, 6); + AssertIsProperty(result[3], "Baz"); + } + + private void AssertIsProperty(ExpressionNode node, string name) + { + Assert.IsType(node); + + var p = (PropertyAccessorNode)node; + Assert.Equal(name, p.PropertyName); + } + + private void AssertIsIndexer(ExpressionNode node, params object[] args) + { + Assert.IsType(node); + + var e = (IndexerNode)node; + Assert.Equal(e.Arguments.ToArray(), args.ToArray()); + } + + private List ToList(ExpressionNode node) + { + var result = new List(); + + while (node != null) + { + result.Add(node); + node = node.Next; + } + + return result; + } + } +} diff --git a/tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests_Errors.cs b/tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests_Errors.cs new file mode 100644 index 0000000000..d30bd097a7 --- /dev/null +++ b/tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests_Errors.cs @@ -0,0 +1,74 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Perspex.Markup.Binding; +using Xunit; + +namespace Perspex.Markup.UnitTests.Binding +{ + public class ExpressionNodeBuilderTests_Errors + { + [Fact] + public void Identifier_Cannot_Start_With_Digit() + { + Assert.Throws( + () => ExpressionNodeBuilder.Build("1Foo")); + } + + [Fact] + public void Identifier_Cannot_Start_With_Symbol() + { + Assert.Throws( + () => ExpressionNodeBuilder.Build("Foo.%Bar")); + } + + [Fact] + public void Expression_Cannot_End_With_Period() + { + Assert.Throws( + () => ExpressionNodeBuilder.Build("Foo.Bar.")); + } + + [Fact] + public void Expression_Cannot_Have_Empty_Indexer() + { + Assert.Throws( + () => ExpressionNodeBuilder.Build("Foo.Bar[]")); + } + + [Fact] + public void Expression_Cannot_Have_Extra_Comma_At_Start_Of_Indexer() + { + Assert.Throws( + () => ExpressionNodeBuilder.Build("Foo.Bar[,3,4]")); + } + + [Fact] + public void Expression_Cannot_Have_Extra_Comma_In_Indexer() + { + Assert.Throws( + () => ExpressionNodeBuilder.Build("Foo.Bar[3,,4]")); + } + + [Fact] + public void Expression_Cannot_Have_Extra_Comma_At_End_Of_Indexer() + { + Assert.Throws( + () => ExpressionNodeBuilder.Build("Foo.Bar[3,4,]")); + } + + [Fact] + public void Expression_Cannot_Have_Digit_After_Indexer() + { + Assert.Throws( + () => ExpressionNodeBuilder.Build("Foo.Bar[3,4]5")); + } + + [Fact] + public void Expression_Cannot_Have_Letter_After_Indexer() + { + Assert.Throws( + () => ExpressionNodeBuilder.Build("Foo.Bar[3,4]A")); + } + } +} diff --git a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Indexer.cs b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Indexer.cs new file mode 100644 index 0000000000..97d9bd1ccc --- /dev/null +++ b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Indexer.cs @@ -0,0 +1,140 @@ +// Copyright (c) The Perspex 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.Collections.ObjectModel; +using System.Reactive.Linq; +using Perspex.Markup.Binding; +using Xunit; + +namespace Perspex.Markup.UnitTests.Binding +{ + public class ExpressionObserverTests_Indexer + { + [Fact] + public async void Should_Get_Array_Value() + { + var data = new { Foo = new [] { "foo", "bar" } }; + var target = new ExpressionObserver(data, "Foo[1]"); + var result = await target.Take(1); + + Assert.Equal("bar", result); + } + + [Fact] + public async void Should_Get_MultiDimensional_Array_Value() + { + var data = new { Foo = new[,] { { "foo", "bar" }, { "baz", "qux" } } }; + var target = new ExpressionObserver(data, "Foo[1, 1]"); + var result = await target.Take(1); + + Assert.Equal("qux", result); + } + + [Fact] + public async void Array_Out_Of_Bounds_Should_Return_UnsetValue() + { + var data = new { Foo = new[] { "foo", "bar" } }; + var target = new ExpressionObserver(data, "Foo[2]"); + var result = await target.Take(1); + + Assert.Equal(PerspexProperty.UnsetValue, result); + } + + [Fact] + public async void Array_With_Wrong_Dimensions_Should_Return_UnsetValue() + { + var data = new { Foo = new[] { "foo", "bar" } }; + var target = new ExpressionObserver(data, "Foo[1,2]"); + var result = await target.Take(1); + + Assert.Equal(PerspexProperty.UnsetValue, result); + } + + [Fact] + public async void List_Out_Of_Bounds_Should_Return_UnsetValue() + { + var data = new { Foo = new List { "foo", "bar" } }; + var target = new ExpressionObserver(data, "Foo[2]"); + var result = await target.Take(1); + + Assert.Equal(PerspexProperty.UnsetValue, result); + } + + [Fact] + public async void Should_Get_List_Value() + { + var data = new { Foo = new List { "foo", "bar" } }; + var target = new ExpressionObserver(data, "Foo[1]"); + var result = await target.Take(1); + + Assert.Equal("bar", result); + } + + [Fact] + public void Should_Track_INCC_Add() + { + var data = new { Foo = new ObservableCollection { "foo", "bar" } }; + var target = new ExpressionObserver(data, "Foo[2]"); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x)); + data.Foo.Add("baz"); + + Assert.Equal(new[] { PerspexProperty.UnsetValue, "baz" }, result); + } + + [Fact] + public void Should_Track_INCC_Remove() + { + var data = new { Foo = new ObservableCollection { "foo", "bar" } }; + var target = new ExpressionObserver(data, "Foo[0]"); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x)); + data.Foo.RemoveAt(0); + + Assert.Equal(new[] { "foo", "bar" }, result); + } + + [Fact] + public void Should_Track_INCC_Replace() + { + var data = new { Foo = new ObservableCollection { "foo", "bar" } }; + var target = new ExpressionObserver(data, "Foo[1]"); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x)); + data.Foo[1] = "baz"; + + Assert.Equal(new[] { "bar", "baz" }, result); + } + + [Fact] + public void Should_Track_INCC_Move() + { + var data = new { Foo = new ObservableCollection { "foo", "bar" } }; + var target = new ExpressionObserver(data, "Foo[1]"); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x)); + data.Foo.Move(0, 1); + + Assert.Equal(new[] { "bar", "foo" }, result); + } + + [Fact] + public void Should_Track_INCC_Reset() + { + var data = new { Foo = new ObservableCollection { "foo", "bar" } }; + var target = new ExpressionObserver(data, "Foo[1]"); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x)); + data.Foo.Clear(); + + Assert.Equal(new[] { "bar", PerspexProperty.UnsetValue }, result); + } + } +} diff --git a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Negation.cs b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Negation.cs new file mode 100644 index 0000000000..2019b884b0 --- /dev/null +++ b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Negation.cs @@ -0,0 +1,92 @@ +// Copyright (c) The Perspex 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 Perspex.Markup.Binding; +using Xunit; + +namespace Perspex.Markup.UnitTests.Binding +{ + public class ExpressionObserverTests_Negation + { + [Fact] + public async void Should_Negate_Boolean_Value() + { + var data = new { Foo = true }; + var target = new ExpressionObserver(data, "!Foo"); + var result = await target.Take(1); + + Assert.Equal(false, result); + } + + [Fact] + public async void Should_Negate_0() + { + var data = new { Foo = 0 }; + var target = new ExpressionObserver(data, "!Foo"); + var result = await target.Take(1); + + Assert.Equal(true, result); + } + + [Fact] + public async void Should_Negate_1() + { + var data = new { Foo = 1 }; + var target = new ExpressionObserver(data, "!Foo"); + var result = await target.Take(1); + + Assert.Equal(false, result); + } + + [Fact] + public async void Should_Negate_False_String() + { + var data = new { Foo = "false" }; + var target = new ExpressionObserver(data, "!Foo"); + var result = await target.Take(1); + + Assert.Equal(true, result); + } + + [Fact] + public async void Should_Negate_True_String() + { + var data = new { Foo = "True" }; + var target = new ExpressionObserver(data, "!Foo"); + var result = await target.Take(1); + + Assert.Equal(false, result); + } + + [Fact] + public async void Should_Return_Empty_For_String_Not_Convertible_To_Boolean() + { + var data = new { Foo = "foo" }; + var target = new ExpressionObserver(data, "!Foo"); + var result = await target.Take(1); + + Assert.Equal(PerspexProperty.UnsetValue, result); + } + + [Fact] + public async void Should_Return_Empty_For_Value_Not_Convertible_To_Boolean() + { + var data = new { Foo = new object() }; + var target = new ExpressionObserver(data, "!Foo"); + var result = await target.Take(1); + + Assert.Equal(PerspexProperty.UnsetValue, result); + } + + [Fact] + public void SetValue_Should_Throw() + { + var data = new { Foo = "foo" }; + var target = new ExpressionObserver(data, "!Foo"); + + Assert.Throws(() => target.SetValue("bar")); + } + } +} diff --git a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Observable.cs b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Observable.cs new file mode 100644 index 0000000000..9296b5aa7c --- /dev/null +++ b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Observable.cs @@ -0,0 +1,68 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using Perspex.Markup.Binding; +using Xunit; + +namespace Perspex.Markup.UnitTests.Binding +{ + public class ExpressionObserverTests_Observable + { + [Fact] + public void Should_Get_Simple_Observable_Value() + { + using (var sync = UnitTestSynchronizationContext.Begin()) + { + var source = new BehaviorSubject("foo"); + var data = new { Foo = source }; + var target = new ExpressionObserver(data, "Foo"); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x)); + source.OnNext("bar"); + sync.ExecutePostedCallbacks(); + + Assert.Equal(new[] { PerspexProperty.UnsetValue, "foo", "bar" }, result); + } + } + + [Fact] + public void Should_Get_Property_Value_From_Observable() + { + using (var sync = UnitTestSynchronizationContext.Begin()) + { + var data = new Class1(); + var target = new ExpressionObserver(data, "Next.Foo"); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x)); + data.Next.OnNext(new Class2("foo")); + sync.ExecutePostedCallbacks(); + + Assert.Equal(new[] { PerspexProperty.UnsetValue, "foo" }, result); + + sub.Dispose(); + Assert.Equal(0, data.SubscriptionCount); + } + } + + private class Class1 : NotifyingBase + { + public Subject Next { get; } = new Subject(); + } + + private class Class2 : NotifyingBase + { + public Class2(string foo) + { + Foo = foo; + } + + public string Foo { get; } + } + } +} diff --git a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Property.cs b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Property.cs new file mode 100644 index 0000000000..a782578505 --- /dev/null +++ b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Property.cs @@ -0,0 +1,281 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Reactive.Linq; +using Perspex.Markup.Binding; +using Xunit; + +namespace Perspex.Markup.UnitTests.Binding +{ + public class ExpressionObserverTests_Property + { + [Fact] + public async void Should_Get_Simple_Property_Value() + { + var data = new { Foo = "foo" }; + var target = new ExpressionObserver(data, "Foo"); + var result = await target.Take(1); + + Assert.Equal("foo", result); + } + + [Fact] + public async void Should_Get_Simple_Property_From_Base_Class() + { + var data = new Class3 { Foo = "foo" }; + var target = new ExpressionObserver(data, "Foo"); + var result = await target.Take(1); + + Assert.Equal("foo", result); + } + + [Fact] + public async void Should_Get_Simple_Property_Chain() + { + var data = new { Foo = new { Bar = new { Baz = "baz" } } }; + var target = new ExpressionObserver(data, "Foo.Bar.Baz"); + var result = await target.Take(1); + + Assert.Equal("baz", result); + } + + [Fact] + public async void Should_Not_Have_Value_For_Broken_Chain() + { + var data = new { Foo = new { Bar = 1 } }; + var target = new ExpressionObserver(data, "Foo.Bar.Baz"); + var result = await target.Take(1); + + Assert.Equal(PerspexProperty.UnsetValue, result); + } + + [Fact] + public void Should_Track_Simple_Property_Value() + { + var data = new Class1 { Foo = "foo" }; + var target = new ExpressionObserver(data, "Foo"); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x)); + data.Foo = "bar"; + + Assert.Equal(new[] { "foo", "bar" }, result); + + sub.Dispose(); + + Assert.Equal(0, data.SubscriptionCount); + } + + [Fact] + public void Should_Track_End_Of_Property_Chain_Changing() + { + var data = new Class1 { Next = new Class2 { Bar = "bar" } }; + var target = new ExpressionObserver(data, "Next.Bar"); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x)); + ((Class2)data.Next).Bar = "baz"; + + Assert.Equal(new[] { "bar", "baz" }, result); + + sub.Dispose(); + + Assert.Equal(0, data.SubscriptionCount); + Assert.Equal(0, data.Next.SubscriptionCount); + } + + [Fact] + public void Should_Track_Property_Chain_Changing() + { + var data = new Class1 { Next = new Class2 { Bar = "bar" } }; + var target = new ExpressionObserver(data, "Next.Bar"); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x)); + var old = data.Next; + data.Next = new Class2 { Bar = "baz" }; + + Assert.Equal(new[] { "bar", "baz" }, result); + + sub.Dispose(); + + Assert.Equal(0, data.SubscriptionCount); + Assert.Equal(0, data.Next.SubscriptionCount); + Assert.Equal(0, old.SubscriptionCount); + } + + [Fact] + public void Should_Track_Property_Chain_Breaking_With_Null_Then_Mending() + { + var data = new Class1 { Next = new Class2 { Bar = "bar" } }; + var target = new ExpressionObserver(data, "Next.Bar"); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x)); + var old = data.Next; + data.Next = null; + data.Next = new Class2 { Bar = "baz" }; + + Assert.Equal(new[] { "bar", PerspexProperty.UnsetValue, "baz" }, result); + + sub.Dispose(); + + Assert.Equal(0, data.SubscriptionCount); + Assert.Equal(0, data.Next.SubscriptionCount); + Assert.Equal(0, old.SubscriptionCount); + } + + [Fact] + public void Should_Track_Property_Chain_Breaking_With_Object_Then_Mending() + { + var data = new Class1 { Next = new Class2 { Bar = "bar" } }; + var target = new ExpressionObserver(data, "Next.Bar"); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x)); + var old = data.Next; + var breaking = new WithoutBar(); + data.Next = breaking; + data.Next = new Class2 { Bar = "baz" }; + + Assert.Equal(new[] { "bar", PerspexProperty.UnsetValue, "baz" }, result); + + sub.Dispose(); + + Assert.Equal(0, data.SubscriptionCount); + Assert.Equal(0, data.Next.SubscriptionCount); + Assert.Equal(0, breaking.SubscriptionCount); + Assert.Equal(0, old.SubscriptionCount); + } + + [Fact] + public void SetValue_Should_Set_Simple_Property_Value() + { + var data = new Class1 { Foo = "foo" }; + var target = new ExpressionObserver(data, "Foo"); + + Assert.True(target.SetValue("bar")); + Assert.Equal("bar", data.Foo); + } + + [Fact] + public void SetValue_Should_Set_Property_At_The_End_Of_Chain() + { + var data = new Class1 { Next = new Class2 { Bar = "bar" } }; + var target = new ExpressionObserver(data, "Next.Bar"); + + Assert.True(target.SetValue("baz")); + Assert.Equal("baz", ((Class2)data.Next).Bar); + } + + [Fact] + public void SetValue_Should_Return_False_For_Missing_Property() + { + var data = new Class1 { Next = new WithoutBar()}; + var target = new ExpressionObserver(data, "Next.Bar"); + + Assert.False(target.SetValue("baz")); + } + + [Fact] + public void SetValue_Should_Return_False_For_Missing_Object() + { + var data = new Class1(); + var target = new ExpressionObserver(data, "Next.Bar"); + + Assert.False(target.SetValue("baz")); + } + + [Fact] + public void SetValue_Should_Throw_For_Wrong_Type() + { + var data = new Class1 { Foo = "foo" }; + var target = new ExpressionObserver(data, "Foo"); + + Assert.Throws(() => target.SetValue(1.2)); + } + + [Fact] + public async void Should_Handle_Null_Root() + { + var target = new ExpressionObserver(null, "Foo"); + var result = await target.Take(1); + + Assert.Equal(PerspexProperty.UnsetValue, result); + } + + [Fact] + public void Can_Replace_Root() + { + var first = new Class1 { Foo = "foo" }; + var second = new Class1 { Foo = "bar" }; + var target = new ExpressionObserver(first, "Foo"); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x)); + target.Root = second; + target.Root = null; + + Assert.Equal(new[] { "foo", "bar", PerspexProperty.UnsetValue }, result); + + Assert.Equal(0, first.SubscriptionCount); + Assert.Equal(0, second.SubscriptionCount); + } + + private interface INext + { + int SubscriptionCount { get; } + } + + private class Class1 : NotifyingBase + { + private string _foo; + private INext _next; + + public string Foo + { + get { return _foo; } + set + { + _foo = value; + RaisePropertyChanged(nameof(Foo)); + } + } + + public INext Next + { + get { return _next; } + set + { + _next = value; + RaisePropertyChanged(nameof(Next)); + } + } + } + + private class Class2 : NotifyingBase, INext + { + private string _bar; + + public string Bar + { + get { return _bar; } + set + { + _bar = value; + RaisePropertyChanged(nameof(Bar)); + } + } + } + + private class Class3 : Class1 + { + } + + private class WithoutBar : NotifyingBase, INext + { + } + } +} diff --git a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Task.cs b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Task.cs new file mode 100644 index 0000000000..7f6e153a5f --- /dev/null +++ b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Task.cs @@ -0,0 +1,87 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Reactive.Linq; +using System.Threading; +using System.Threading.Tasks; +using Perspex.Markup.Binding; +using Xunit; + +namespace Perspex.Markup.UnitTests.Binding +{ + public class ExpressionObserverTests_Task + { + [Fact] + public void Should_Get_Simple_Task_Value() + { + using (var sync = UnitTestSynchronizationContext.Begin()) + { + var tcs = new TaskCompletionSource(); + var data = new { Foo = tcs.Task }; + var target = new ExpressionObserver(data, "Foo"); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x)); + tcs.SetResult("foo"); + sync.ExecutePostedCallbacks(); + + Assert.Equal(new object[] { PerspexProperty.UnsetValue, "foo" }, result.ToArray()); + } + } + + [Fact] + public void Should_Get_Completed_Task_Value() + { + using (var sync = UnitTestSynchronizationContext.Begin()) + { + var data = new { Foo = Task.FromResult("foo") }; + var target = new ExpressionObserver(data, "Foo"); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x)); + + Assert.Equal(new object[] { "foo" }, result.ToArray()); + } + } + + [Fact] + public void Should_Get_Property_Value_From_Task() + { + using (var sync = UnitTestSynchronizationContext.Begin()) + { + var tcs = new TaskCompletionSource(); + var data = new Class1(tcs.Task); + var target = new ExpressionObserver(data, "Next.Foo"); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x)); + tcs.SetResult(new Class2("foo")); + sync.ExecutePostedCallbacks(); + + Assert.Equal(new object[] { PerspexProperty.UnsetValue, "foo" }, result.ToArray()); + } + } + + private class Class1 : NotifyingBase + { + public Class1(Task next) + { + Next = next; + } + + public Task Next { get; } + } + + private class Class2 : NotifyingBase + { + public Class2(string foo) + { + Foo = foo; + } + + public string Foo { get; } + } + } +} diff --git a/tests/Perspex.Markup.UnitTests/Binding/NotifyingBase.cs b/tests/Perspex.Markup.UnitTests/Binding/NotifyingBase.cs new file mode 100644 index 0000000000..5cc6a32fea --- /dev/null +++ b/tests/Perspex.Markup.UnitTests/Binding/NotifyingBase.cs @@ -0,0 +1,42 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.ComponentModel; +using System.Linq; + +namespace Perspex.Markup.UnitTests.Binding +{ + public class NotifyingBase : INotifyPropertyChanged + { + private PropertyChangedEventHandler _propertyChanged; + + public event PropertyChangedEventHandler PropertyChanged + { + add + { + _propertyChanged += value; + ++SubscriptionCount; + } + + remove + { + if (_propertyChanged?.GetInvocationList().Contains(value) == true) + { + _propertyChanged -= value; + --SubscriptionCount; + } + } + } + + public int SubscriptionCount + { + get; + private set; + } + + protected void RaisePropertyChanged(string propertyName) + { + _propertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj b/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj new file mode 100644 index 0000000000..36ff242096 --- /dev/null +++ b/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj @@ -0,0 +1,117 @@ + + + + + + + Debug + AnyCPU + {8EF392D5-1416-45AA-9956-7CBBC3229E8A} + Library + Properties + Perspex.Markup.UnitTests + Perspex.Markup.UnitTests + v4.6 + 512 + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + ..\..\packages\Rx-Core.2.2.5\lib\net45\System.Reactive.Core.dll + True + + + ..\..\packages\Rx-Interfaces.2.2.5\lib\net45\System.Reactive.Interfaces.dll + True + + + ..\..\packages\Rx-Linq.2.2.5\lib\net45\System.Reactive.Linq.dll + True + + + ..\..\packages\Rx-PlatformServices.2.2.5\lib\net45\System.Reactive.PlatformServices.dll + True + + + + + + + + + ..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll + True + + + ..\..\packages\xunit.assert.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll + True + + + ..\..\packages\xunit.extensibility.core.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.dll + True + + + + + + + + + + + + + + + + + + + + {6417e941-21bc-467b-a771-0de389353ce6} + Perspex.Markup + + + {b09b78d8-9b26-48b0-9149-d64a2f120f3f} + Perspex.Base + + + + + + + + + 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}. + + + + + + \ No newline at end of file diff --git a/tests/Perspex.Markup.UnitTests/Properties/AssemblyInfo.cs b/tests/Perspex.Markup.UnitTests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..34123fa8b5 --- /dev/null +++ b/tests/Perspex.Markup.UnitTests/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("Perspex.Markup.UnitTests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Perspex.Markup.UnitTests")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[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("8ef392d5-1416-45aa-9956-7cbbc3229e8a")] + +// 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/tests/Perspex.Markup.UnitTests/UnitTestSynchronizationContext.cs b/tests/Perspex.Markup.UnitTests/UnitTestSynchronizationContext.cs new file mode 100644 index 0000000000..1a1eaf4ecf --- /dev/null +++ b/tests/Perspex.Markup.UnitTests/UnitTestSynchronizationContext.cs @@ -0,0 +1,68 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Reactive.Disposables; +using System.Threading; + +namespace Perspex.Markup.UnitTests +{ + internal sealed class UnitTestSynchronizationContext : SynchronizationContext + { + readonly List> _postedCallbacks = + new List>(); + + public static Scope Begin() + { + var sync = new UnitTestSynchronizationContext(); + var old = SynchronizationContext.Current; + SynchronizationContext.SetSynchronizationContext(sync); + return new Scope(old, sync); + } + + public override void Send(SendOrPostCallback d, object state) + { + d(state); + } + + public override void Post(SendOrPostCallback d, object state) + { + lock (_postedCallbacks) + { + _postedCallbacks.Add(Tuple.Create(d, state)); + } + } + + public void ExecutePostedCallbacks() + { + lock (_postedCallbacks) + { + _postedCallbacks.ForEach(t => t.Item1(t.Item2)); + _postedCallbacks.Clear(); + } + } + + public class Scope : IDisposable + { + private SynchronizationContext _old; + private UnitTestSynchronizationContext _new; + + public Scope(SynchronizationContext old, UnitTestSynchronizationContext n) + { + _old = old; + _new = n; + } + + public void Dispose() + { + SynchronizationContext.SetSynchronizationContext(_old); + } + + public void ExecutePostedCallbacks() + { + _new.ExecutePostedCallbacks(); + } + } + } +} diff --git a/tests/Perspex.Markup.UnitTests/packages.config b/tests/Perspex.Markup.UnitTests/packages.config new file mode 100644 index 0000000000..0d1a6f02e7 --- /dev/null +++ b/tests/Perspex.Markup.UnitTests/packages.config @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Perspex.Markup.Xaml.UnitTests/BinderTest.cs b/tests/Perspex.Markup.Xaml.UnitTests/BinderTest.cs deleted file mode 100644 index 4a421bce2f..0000000000 --- a/tests/Perspex.Markup.Xaml.UnitTests/BinderTest.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) The Perspex Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using Moq; -using Perspex.Markup.Xaml.DataBinding; -using OmniXaml.TypeConversion; -using Xunit; - -namespace Perspex.Xaml.Base.UnitTest -{ - public class BinderTest - { - [Fact] - public void NullTarget_Throws() - { - var typeConverter = new Mock(); - var perspexPropertyBinder = new PerspexPropertyBinder(typeConverter.Object); - var bindingDefinitionBuilder = new BindingDefinitionBuilder(); - var binding = bindingDefinitionBuilder - .WithNullTarget() - .Build(); - - var exception = Assert.Throws(() => perspexPropertyBinder.Create(binding)); - } - } -} diff --git a/tests/Perspex.Markup.Xaml.UnitTests/Binding/XamlBindingTests.cs b/tests/Perspex.Markup.Xaml.UnitTests/Binding/XamlBindingTests.cs new file mode 100644 index 0000000000..f78a7ed4ae --- /dev/null +++ b/tests/Perspex.Markup.Xaml.UnitTests/Binding/XamlBindingTests.cs @@ -0,0 +1,161 @@ +// Copyright (c) The Perspex 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 Moq; +using Perspex.Controls; +using Perspex.Markup.Xaml.Binding; +using Xunit; + +namespace Perspex.Markup.Xaml.UnitTests.Binding +{ + public class XamlBindingTests + { + [Fact] + public void OneWay_Binding_Should_Be_Set_Up() + { + var target = CreateTarget(); + var binding = new XamlBinding + { + SourcePropertyPath = "Foo", + BindingMode = BindingMode.OneWay, + }; + + binding.Bind(target.Object, TextBox.TextProperty); + + target.Verify(x => x.Bind( + TextBox.TextProperty, + It.IsAny>(), + BindingPriority.LocalValue)); + } + + [Fact] + public void TwoWay_Binding_Should_Be_Set_Up() + { + var target = CreateTarget(); + var binding = new XamlBinding + { + SourcePropertyPath = "Foo", + BindingMode = BindingMode.TwoWay, + }; + + binding.Bind(target.Object, TextBox.TextProperty); + + target.Verify(x => x.BindTwoWay( + TextBox.TextProperty, + It.IsAny>(), + BindingPriority.LocalValue)); + } + + [Fact] + public void OneTime_Binding_Should_Be_Set_Up() + { + var dataContext = new BehaviorSubject(null); + var expression = new BehaviorSubject(null); + var target = CreateTarget(dataContext: dataContext); + var binding = new XamlBinding + { + SourcePropertyPath = "Foo", + BindingMode = BindingMode.OneTime, + }; + + binding.Bind(target.Object, TextBox.TextProperty, expression); + + target.Verify(x => x.SetValue( + (PerspexProperty)TextBox.TextProperty, + null, + BindingPriority.LocalValue)); + target.ResetCalls(); + + expression.OnNext("foo"); + dataContext.OnNext(1); + + target.Verify(x => x.SetValue( + (PerspexProperty)TextBox.TextProperty, + "foo", + BindingPriority.LocalValue)); + } + + [Fact] + public void OneWayToSource_Binding_Should_Be_Set_Up() + { + var textObservable = new Mock>(); + var expression = new Mock>(); + var target = CreateTarget(text: textObservable.Object); + var binding = new XamlBinding + { + SourcePropertyPath = "Foo", + BindingMode = BindingMode.OneWayToSource, + }; + + binding.Bind(target.Object, TextBox.TextProperty, expression.Object); + + textObservable.Verify(x => x.Subscribe(expression.Object)); + } + + [Fact] + public void Default_BindingMode_Should_Be_Used() + { + var target = CreateTarget(null); + var binding = new XamlBinding + { + SourcePropertyPath = "Foo", + }; + + binding.Bind(target.Object, TextBox.TextProperty); + + // Default for TextBox.Text is two-way. + target.Verify(x => x.BindTwoWay( + TextBox.TextProperty, + It.IsAny>(), + BindingPriority.LocalValue)); + } + + [Fact] + public void DataContext_Binding_Should_Use_Parent_DataContext() + { + var parentDataContext = Mock.Of(x => x.Header == (object)"Foo"); + + var parent = new Decorator + { + Child = new Control(), + DataContext = parentDataContext, + }; + + var binding = new XamlBinding + { + SourcePropertyPath = "Header", + }; + + binding.Bind(parent.Child, Control.DataContextProperty); + + Assert.Equal("Foo", parent.Child.DataContext); + + parentDataContext = Mock.Of(x => x.Header == (object)"Bar"); + parent.DataContext = parentDataContext; + Assert.Equal("Bar", parent.Child.DataContext); + } + + private Mock CreateTarget(object dataContext) + { + return CreateTarget(dataContext: Observable.Never().StartWith(dataContext)); + } + + private Mock CreateTarget( + IObservable dataContext = null, + IObservable text = null) + { + var result = new Mock(); + + dataContext = dataContext ?? Observable.Never().StartWith((object)null); + text = text ?? Observable.Never().StartWith((string)null); + + result.Setup(x => x.GetObservable(Control.DataContextProperty)).Returns(dataContext); + result.Setup(x => x.GetObservable((PerspexProperty)Control.DataContextProperty)).Returns(dataContext); + result.Setup(x => x.GetObservable((PerspexProperty)TextBox.TextProperty)).Returns(text); + return result; + } + } +} diff --git a/tests/Perspex.Markup.Xaml.UnitTests/BindingDefinitionBuilder.cs b/tests/Perspex.Markup.Xaml.UnitTests/BindingDefinitionBuilder.cs deleted file mode 100644 index e968db3615..0000000000 --- a/tests/Perspex.Markup.Xaml.UnitTests/BindingDefinitionBuilder.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) The Perspex Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using Perspex.Controls; -using Perspex.Markup.Xaml.DataBinding; -using Perspex.Markup.Xaml.DataBinding.ChangeTracking; - -namespace Perspex.Xaml.Base.UnitTest -{ - public class BindingDefinitionBuilder - { - private readonly BindingMode _bindingMode; - private readonly PropertyPath _sourcePropertyPath; - private Control _target; - private PerspexProperty _targetProperty; - - public BindingDefinitionBuilder() - { - _bindingMode = BindingMode.Default; - _sourcePropertyPath = new PropertyPath(string.Empty); - } - - public BindingDefinitionBuilder WithNullTarget() - { - _target = null; - return this; - } - - public XamlBindingDefinition Build() - { - return new XamlBindingDefinition( - bindingMode: _bindingMode, - sourcePropertyPath: _sourcePropertyPath, - target: _target, - targetProperty: _targetProperty); - } - } -} \ No newline at end of file diff --git a/tests/Perspex.Markup.Xaml.UnitTests/ChangeBranchTest.cs b/tests/Perspex.Markup.Xaml.UnitTests/ChangeBranchTest.cs deleted file mode 100644 index 202cfed999..0000000000 --- a/tests/Perspex.Markup.Xaml.UnitTests/ChangeBranchTest.cs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) The Perspex Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using Perspex.Markup.Xaml.DataBinding.ChangeTracking; -using Perspex.Xaml.Base.UnitTest.SampleModel; -using Xunit; - -namespace Perspex.Xaml.Base.UnitTest -{ - public class ChangeBranchTest - { - [Fact] - public void GetValueOfMemberOfStruct() - { - var level1 = new Level1(); - level1.DateTime = new DateTime(1, 2, 3, 4, 5, 6); - - var branch = new ObservablePropertyBranch(level1, new PropertyPath("DateTime.Minute")); - - var day = branch.Value; - Assert.Equal(day, branch.Value); - } - - [Fact] - public void OnePathOnly() - { - var level1 = new Level1(); - - var branch = new ObservablePropertyBranch(level1, new PropertyPath("Text")); - var newValue = "Hey now"; - branch.Value = newValue; - - Assert.Equal(level1.Text, newValue); - } - - [Fact] - public void SettingValueToUnderlyingProperty_ChangesTheValueInBranch() - { - var level1 = new Level1(); - - level1.Level2.Level3.Property = 3; - - var branch = new ObservablePropertyBranch(level1, new PropertyPath("Level2.Level3.Property")); - Assert.Equal(3, branch.Value); - } - - [Fact] - public void SettingValueToBranch_ChangesTheUnderlyingProperty() - { - var level1 = new Level1(); - - var branch = new ObservablePropertyBranch(level1, new PropertyPath("Level2.Level3.Property")); - branch.Value = 3; - Assert.Equal(3, level1.Level2.Level3.Property); - } - - [Fact] - public void SettingValueProperty_RaisesChangeInBranch() - { - var level1 = new Level1(); - - var branch = new ObservablePropertyBranch(level1, new PropertyPath("Level2.Level3.Property")); - bool received = false; - ObservableExtensions.Subscribe(branch.Values, v => received = ((int)v == 3)); - - level1.Level2.Level3.Property = 3; - - Assert.True(received); - } - } -} diff --git a/tests/Perspex.Markup.Xaml.UnitTests/DataContextChangeSynchronizerTest.cs b/tests/Perspex.Markup.Xaml.UnitTests/DataContextChangeSynchronizerTest.cs deleted file mode 100644 index 0b93a2421d..0000000000 --- a/tests/Perspex.Markup.Xaml.UnitTests/DataContextChangeSynchronizerTest.cs +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright (c) The Perspex Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using System; -using Perspex.Controls; -using GitHubClient.ViewModels; -using Perspex.Markup.Xaml.DataBinding; -using Perspex.Markup.Xaml.DataBinding.ChangeTracking; -using OmniXaml.Builder; -using OmniXaml.TypeConversion; -using OmniXaml.TypeConversion.BuiltInConverters; -using Perspex.Xaml.Base.UnitTest.SampleModel; -using Xunit; - -namespace Perspex.Xaml.Base.UnitTest -{ - public class DataContextChangeSynchronizerTest - { - private readonly TypeConverterProvider _repo; - private readonly SamplePerspexObject _guiObject; - private readonly ViewModelMock _viewModel; - - public DataContextChangeSynchronizerTest() - { - _repo = new TypeConverterProvider(); - _guiObject = new SamplePerspexObject(); - _viewModel = new ViewModelMock(); - } - - [Fact] - public void SameTypesFromUIToModel() - { - var synchronizer = new DataContextChangeSynchronizer(new DataContextChangeSynchronizer.BindingSource(new PropertyPath("IntProp"), _viewModel), new DataContextChangeSynchronizer.BindingTarget(_guiObject, SamplePerspexObject.IntProperty), _repo); - synchronizer.StartUpdatingSourceWhenTargetChanges(); - - const int someValue = 4; - _guiObject.Int = someValue; - - Assert.Equal(someValue, _viewModel.IntProp); - } - - [Fact] - public void DifferentTypesFromUIToModel() - { - var synchronizer = new DataContextChangeSynchronizer(new DataContextChangeSynchronizer.BindingSource(new PropertyPath("IntProp"), _viewModel), new DataContextChangeSynchronizer.BindingTarget(_guiObject, SamplePerspexObject.StringProperty), _repo); - synchronizer.StartUpdatingSourceWhenTargetChanges(); - - _guiObject.String = "2"; - - Assert.Equal(2, _viewModel.IntProp); - } - - [Fact] - public void DifferentTypesAndNonConvertibleValueFromUIToModel() - { - var synchronizer = new DataContextChangeSynchronizer(new DataContextChangeSynchronizer.BindingSource(new PropertyPath("IntProp"), _viewModel), new DataContextChangeSynchronizer.BindingTarget(_guiObject, SamplePerspexObject.StringProperty), _repo); - synchronizer.StartUpdatingSourceWhenTargetChanges(); - - _guiObject.String = ""; - - Assert.Equal(default(int), _viewModel.IntProp); - } - - - [Fact] - public void DifferentTypesFromModelToUI() - { - var synchronizer = new DataContextChangeSynchronizer(new DataContextChangeSynchronizer.BindingSource(new PropertyPath("IntProp"), _viewModel), new DataContextChangeSynchronizer.BindingTarget(_guiObject, SamplePerspexObject.StringProperty), _repo); - synchronizer.StartUpdatingTargetWhenSourceChanges(); - - _viewModel.IntProp = 2; - - Assert.Equal("2", _guiObject.String); - } - - [Fact] - public void SameTypesFromModelToUI() - { - var synchronizer = new DataContextChangeSynchronizer(new DataContextChangeSynchronizer.BindingSource(new PropertyPath("IntProp"), _viewModel), new DataContextChangeSynchronizer.BindingTarget(_guiObject, SamplePerspexObject.IntProperty), _repo); - synchronizer.StartUpdatingTargetWhenSourceChanges(); - - _viewModel.IntProp = 2; - - Assert.Equal(2, _guiObject.Int); - } - - [Fact] - public void GrokysTest() - { - var mainWindowViewModel = new MainWindowViewModel(); - var contentControl = new ContentControl(); - - var synchronizer = new DataContextChangeSynchronizer(new DataContextChangeSynchronizer.BindingSource(new PropertyPath("Content"), mainWindowViewModel), new DataContextChangeSynchronizer.BindingTarget(contentControl, ContentControl.ContentProperty), _repo); - - synchronizer.StartUpdatingTargetWhenSourceChanges(); - - var logInViewModel = new LogInViewModel(); - mainWindowViewModel.Content = logInViewModel; - - Assert.Equal(logInViewModel, contentControl.Content); - } - } -} diff --git a/tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj b/tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj index 3d489dd682..2ac180603f 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj +++ b/tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj @@ -46,13 +46,14 @@ ..\..\packages\Octokit.0.14.0\lib\net45\Octokit.dll + + ..\..\packages\Splat.1.6.2\lib\Net45\Splat.dll + True + ..\..\packages\Moq.4.2.1409.1722\lib\net40\Moq.dll - - ..\..\packages\Splat.1.6.1\lib\Net45\Splat.dll - ..\..\packages\Sprache.2.0.0.47\lib\portable-net4+netcore45+win8+wp8+sl5+MonoAndroid1+MonoTouch1\Sprache.dll @@ -87,10 +88,8 @@ - - + - @@ -103,9 +102,7 @@ - - diff --git a/tests/Perspex.Markup.Xaml.UnitTests/PropertyMountPointTest.cs b/tests/Perspex.Markup.Xaml.UnitTests/PropertyMountPointTest.cs deleted file mode 100644 index 1b15094af5..0000000000 --- a/tests/Perspex.Markup.Xaml.UnitTests/PropertyMountPointTest.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) The Perspex Project. All rights reserved. -// Licensed under the MIT license. See licence.md file in the project root for full license information. - -using Perspex.Markup.Xaml.DataBinding.ChangeTracking; -using System; -using Xunit; - -namespace Perspex.Xaml.Base.UnitTest -{ - public class PropertyMountPointTest - { - [Fact] - public void SourceAndPathAreNull() - { - Assert.Throws(() => new PropertyMountPoint(null, null)); - } - } -} diff --git a/tests/Perspex.Markup.Xaml.UnitTests/XamlBindingTest.cs b/tests/Perspex.Markup.Xaml.UnitTests/XamlBindingTest.cs index d7ee56245f..6e0d14d5cf 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/XamlBindingTest.cs +++ b/tests/Perspex.Markup.Xaml.UnitTests/XamlBindingTest.cs @@ -2,7 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using Moq; -using Perspex.Markup.Xaml.DataBinding; +using Perspex.Markup.Xaml.Binding; using OmniXaml.TypeConversion; using Xunit; @@ -13,9 +13,9 @@ namespace Perspex.Xaml.Base.UnitTest [Fact] public void TestNullDataContext() { - var t = new Mock(); - var sut = new XamlBinding(t.Object); - sut.BindToDataContext(null); + //var t = new Mock(); + //var sut = new XamlBinding(t.Object); + //sut.BindTo(null); } } } diff --git a/tests/Perspex.Markup.Xaml.UnitTests/packages.config b/tests/Perspex.Markup.Xaml.UnitTests/packages.config index c7f996e816..f0bea65dab 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/packages.config +++ b/tests/Perspex.Markup.Xaml.UnitTests/packages.config @@ -6,7 +6,7 @@ - + diff --git a/tests/Perspex.Styling.UnitTests/SelectorTests_Child.cs b/tests/Perspex.Styling.UnitTests/SelectorTests_Child.cs index 845a712465..0df2e061a3 100644 --- a/tests/Perspex.Styling.UnitTests/SelectorTests_Child.cs +++ b/tests/Perspex.Styling.UnitTests/SelectorTests_Child.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using System.Reactive.Linq; +using System.Reactive.Subjects; using System.Threading.Tasks; using Perspex.Collections; using Perspex.Styling; @@ -87,6 +88,14 @@ namespace Perspex.Styling.UnitTests public ITemplatedControl TemplatedParent { get; } + public IPropertyBag InheritanceParent + { + get + { + throw new NotImplementedException(); + } + } + public IDisposable Bind(PerspexProperty property, IObservable source, BindingPriority priority) { throw new NotImplementedException(); @@ -141,6 +150,16 @@ namespace Perspex.Styling.UnitTests { throw new NotImplementedException(); } + + public IDisposable BindTwoWay(PerspexProperty property, PerspexObject source, PerspexProperty sourceProperty, BindingPriority priority = BindingPriority.LocalValue) + { + throw new NotImplementedException(); + } + + public IDisposable BindTwoWay(PerspexProperty property, ISubject source, BindingPriority priority = BindingPriority.LocalValue) + { + throw new NotImplementedException(); + } } public class TestLogical1 : TestLogical diff --git a/tests/Perspex.Styling.UnitTests/SelectorTests_Descendent.cs b/tests/Perspex.Styling.UnitTests/SelectorTests_Descendent.cs index 74b2a3345a..7b5f78e6bb 100644 --- a/tests/Perspex.Styling.UnitTests/SelectorTests_Descendent.cs +++ b/tests/Perspex.Styling.UnitTests/SelectorTests_Descendent.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using System.Reactive.Linq; +using System.Reactive.Subjects; using System.Threading.Tasks; using Perspex.Collections; using Perspex.Styling; @@ -119,6 +120,14 @@ namespace Perspex.Styling.UnitTests public ITemplatedControl TemplatedParent { get; } + public IPropertyBag InheritanceParent + { + get + { + throw new NotImplementedException(); + } + } + public IDisposable Bind(PerspexProperty property, IObservable source, BindingPriority priority) { throw new NotImplementedException(); @@ -173,6 +182,16 @@ namespace Perspex.Styling.UnitTests { throw new NotImplementedException(); } + + public IDisposable BindTwoWay(PerspexProperty property, PerspexObject source, PerspexProperty sourceProperty, BindingPriority priority = BindingPriority.LocalValue) + { + throw new NotImplementedException(); + } + + public IDisposable BindTwoWay(PerspexProperty property, ISubject source, BindingPriority priority = BindingPriority.LocalValue) + { + throw new NotImplementedException(); + } } public class TestLogical1 : TestLogical diff --git a/tests/Perspex.Styling.UnitTests/TestControlBase.cs b/tests/Perspex.Styling.UnitTests/TestControlBase.cs index 8b621fe533..ad47f1eb27 100644 --- a/tests/Perspex.Styling.UnitTests/TestControlBase.cs +++ b/tests/Perspex.Styling.UnitTests/TestControlBase.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Reactive.Subjects; namespace Perspex.Styling.UnitTests { @@ -27,6 +28,14 @@ namespace Perspex.Styling.UnitTests set; } + public IPropertyBag InheritanceParent + { + get + { + throw new NotImplementedException(); + } + } + public IDisposable Bind(PerspexProperty property, IObservable source, BindingPriority priority) { throw new NotImplementedException(); @@ -81,5 +90,15 @@ namespace Perspex.Styling.UnitTests { throw new NotImplementedException(); } + + public IDisposable BindTwoWay(PerspexProperty property, PerspexObject source, PerspexProperty sourceProperty, BindingPriority priority = BindingPriority.LocalValue) + { + throw new NotImplementedException(); + } + + public IDisposable BindTwoWay(PerspexProperty property, ISubject source, BindingPriority priority = BindingPriority.LocalValue) + { + throw new NotImplementedException(); + } } } diff --git a/tests/Perspex.Styling.UnitTests/TestTemplatedControl.cs b/tests/Perspex.Styling.UnitTests/TestTemplatedControl.cs index 8fd47a27fb..764c402138 100644 --- a/tests/Perspex.Styling.UnitTests/TestTemplatedControl.cs +++ b/tests/Perspex.Styling.UnitTests/TestTemplatedControl.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Reactive.Subjects; namespace Perspex.Styling.UnitTests { @@ -33,6 +34,14 @@ namespace Perspex.Styling.UnitTests get; } + public IPropertyBag InheritanceParent + { + get + { + throw new NotImplementedException(); + } + } + public IObservable GetObservable(PerspexProperty property) { throw new NotImplementedException(); @@ -87,5 +96,15 @@ namespace Perspex.Styling.UnitTests { throw new NotImplementedException(); } + + public IDisposable BindTwoWay(PerspexProperty property, PerspexObject source, PerspexProperty sourceProperty, BindingPriority priority = BindingPriority.LocalValue) + { + throw new NotImplementedException(); + } + + public IDisposable BindTwoWay(PerspexProperty property, ISubject source, BindingPriority priority = BindingPriority.LocalValue) + { + throw new NotImplementedException(); + } } }