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 f9f2f0a09f..6ab0be278c 100644 --- a/Perspex.sln +++ b/Perspex.sln @@ -96,6 +96,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.HtmlRenderer", "src 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.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 @@ -237,6 +243,18 @@ Global {5FB2B005-0A7F-4DAD-ADD4-3ED01444E63D}.Debug|Any CPU.Build.0 = Debug|Any CPU {5FB2B005-0A7F-4DAD-ADD4-3ED01444E63D}.Release|Any CPU.ActiveCfg = Release|Any CPU {5FB2B005-0A7F-4DAD-ADD4-3ED01444E63D}.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 @@ -266,6 +284,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/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..149948db44 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/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..3bf709e928 --- /dev/null +++ b/src/Markup/Perspex.Markup.Xaml/Binding/XamlBinding.cs @@ -0,0 +1,70 @@ +// 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)); + Bind(instance, property, subject); + } + + public ExpressionObserver CreateExpressionObserver(IObservablePropertyBag instance) + { + var result = new ExpressionObserver(null, SourcePropertyPath); + var dataContext = instance.GetObservable(Control.DataContextProperty); + dataContext.Subscribe(x => result.Root = x); + return result; + } + + 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..42b0e3b6ef 160000 --- a/src/Markup/Perspex.Markup.Xaml/OmniXAML +++ b/src/Markup/Perspex.Markup.Xaml/OmniXAML @@ -1 +1 @@ -Subproject commit 49e6ec001f5873cf2290e0bc1f6f06ca9b9cf808 +Subproject commit 42b0e3b6efc0905457120752be38a6000898fffa 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..0868dae69f --- /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().Value 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..e5dc9f2810 --- /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 ExpressionValue _value = ExpressionValue.None; + + 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 = ExpressionValue.None; + } + + if (Next != null) + { + Next.Target = CurrentValue.Value; + } + } + } + + public ExpressionValue CurrentValue + { + get + { + return _value; + } + + set + { + _value = value; + + if (Next != null) + { + Next.Target = value.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 = new ExpressionValue(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..ff98e82aeb --- /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..ac5700e512 --- /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.Select(x => x.Value).Subscribe(observer); + } + } +} diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionValue.cs b/src/Markup/Perspex.Markup/Binding/ExpressionValue.cs new file mode 100644 index 0000000000..2441acda73 --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/ExpressionValue.cs @@ -0,0 +1,38 @@ +// 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 +{ + /// + /// Holds the value for an . + /// + public struct ExpressionValue + { + /// + /// An that has no value. + /// + public static readonly ExpressionValue None = new ExpressionValue(); + + /// + /// Initializes a new instance of the struct. + /// + /// + public ExpressionValue(object value) + { + HasValue = true; + Value = value; + } + + /// + /// Gets a value indicating whether the evaluated expression resulted in a value. + /// + public bool HasValue { get; } + + /// + /// Gets a the result of the expression. + /// + public object Value { get; } + } +} diff --git a/src/Markup/Perspex.Markup/Binding/IndexerNode.cs b/src/Markup/Perspex.Markup/Binding/IndexerNode.cs new file mode 100644 index 0000000000..953852a6db --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/IndexerNode.cs @@ -0,0 +1,106 @@ +// 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 ExpressionValue GetValue(object target) + { + var typeInfo = target.GetType().GetTypeInfo(); + var list = target as IList; + + if (typeInfo.IsArray && _intArgs != null) + { + return new ExpressionValue(((Array)target).GetValue(_intArgs)); + } + else if (target is IList && _intArgs?.Length == 1) + { + if (_intArgs[0] < list.Count) + { + return new ExpressionValue(list[_intArgs[0]]); + } + } + + return ExpressionValue.None; + } + } +} diff --git a/src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs b/src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs new file mode 100644 index 0000000000..d20972c639 --- /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 ExpressionValue Negate(ExpressionValue v) + { + if (v.HasValue) + { + try + { + var boolean = Convert.ToBoolean(v.Value, CultureInfo.InvariantCulture); + return new ExpressionValue(!boolean); + } + catch + { + // TODO: Maybe should log something here. + } + } + + return ExpressionValue.None; + } + } +} 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..08056f4aab --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/Parsers/LiteralParser.cs @@ -0,0 +1,34 @@ +// 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.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()); + } + + 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..af2db5d63a --- /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 = ExpressionValue.None; + } + } + + 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 = ExpressionValue.None; + set = true; + _subscription = observable + .ObserveOn(SynchronizationContext.Current) + .Subscribe(x => CurrentValue = new ExpressionValue(x)); + } + else if (task != null) + { + var resultProperty = task.GetType().GetTypeInfo().GetDeclaredProperty("Result"); + + if (resultProperty != null) + { + if (task.Status == TaskStatus.RanToCompletion) + { + CurrentValue = new ExpressionValue(resultProperty.GetValue(task)); + set = true; + } + else + { + task.ContinueWith( + x => CurrentValue = new ExpressionValue(resultProperty.GetValue(task)), + TaskScheduler.FromCurrentSynchronizationContext()) + .ConfigureAwait(false); + } + } + } + else + { + CurrentValue = new ExpressionValue(value); + set = true; + } + + if (!set) + { + CurrentValue = ExpressionValue.None; + } + } + + 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..a7e6bf61cd --- /dev/null +++ b/src/Markup/Perspex.Markup/Perspex.Markup.csproj @@ -0,0 +1,88 @@ + + + + + 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 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + ..\..\..\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/PerspexObject.cs b/src/Perspex.Base/PerspexObject.cs index 7a0e211f2c..f0abeaf8fa 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; @@ -186,7 +187,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 +335,7 @@ namespace Perspex PropertyChanged -= handler; }); }, - GetObservableDescription(property)); + GetDescription(property)); } /// @@ -377,7 +378,7 @@ namespace Perspex PropertyChanged -= handler; }); }, - GetObservableDescription(property)); + GetDescription(property)); } /// @@ -492,6 +493,7 @@ namespace Perspex throw new ArgumentException($"The property {property.Name} is readonly."); } + LogPropertySet(property, value, priority); property.Setter(this, value); } else @@ -524,14 +526,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 +554,7 @@ namespace Perspex throw new ArgumentException($"The property {property.Name} is readonly."); } + LogPropertySet(property, value, priority); property.Setter(this, value); } else @@ -593,14 +591,13 @@ namespace Perspex _propertyLog.Verbose( "Bound {Property} to {Binding} with priority LocalValue", property, - source); + GetDescription(source)); return source.Subscribe(x => SetValue(property, x)); } else { PriorityValue v; - IDescription description = source as IDescription; if (!IsRegistered(property)) { @@ -616,7 +613,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 +655,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 +673,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 +958,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.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..4f35084d70 100644 --- a/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs @@ -192,14 +192,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/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/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/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/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..e621369679 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. 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.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/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.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.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..f544652c52 --- /dev/null +++ b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Indexer.cs @@ -0,0 +1,113 @@ +// 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.True(result.HasValue); + Assert.Equal("bar", result.Value); + } + + [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.True(result.HasValue); + Assert.Equal("qux", result.Value); + } + + [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.True(result.HasValue); + Assert.Equal("bar", result.Value); + } + + [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.Value)); + data.Foo.Add("baz"); + + Assert.Equal(new[] { null, "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.Value)); + 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.Value)); + 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.Value)); + 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.Value)); + data.Foo.Clear(); + + Assert.Equal(new[] { "bar", null }, 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..614f27b34c --- /dev/null +++ b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Negation.cs @@ -0,0 +1,97 @@ +// 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.True(result.HasValue); + Assert.Equal(false, result.Value); + } + + [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.True(result.HasValue); + Assert.Equal(true, result.Value); + } + + [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.True(result.HasValue); + Assert.Equal(false, result.Value); + } + + [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.True(result.HasValue); + Assert.Equal(true, result.Value); + } + + [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.True(result.HasValue); + Assert.Equal(false, result.Value); + } + + [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.False(result.HasValue); + } + + [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.False(result.HasValue); + } + + [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..979fa3856c --- /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.Value)); + source.OnNext("bar"); + sync.ExecutePostedCallbacks(); + + Assert.Equal(new[] { null, "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.Value)); + data.Next.OnNext(new Class2("foo")); + sync.ExecutePostedCallbacks(); + + Assert.Equal(new[] { null, "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..3518cfa52d --- /dev/null +++ b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Property.cs @@ -0,0 +1,284 @@ +// 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.True(result.HasValue); + Assert.Equal("foo", result.Value); + } + + [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.True(result.HasValue); + Assert.Equal("foo", result.Value); + } + + [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.True(result.HasValue); + Assert.Equal("baz", result.Value); + } + + [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.False(result.HasValue); + } + + [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.Value)); + 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.Value)); + ((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.Value)); + 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.Value)); + var old = data.Next; + data.Next = null; + data.Next = new Class2 { Bar = "baz" }; + + Assert.Equal(new[] { "bar", null, "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.Value)); + var old = data.Next; + var breaking = new WithoutBar(); + data.Next = breaking; + data.Next = new Class2 { Bar = "baz" }; + + Assert.Equal(new[] { "bar", null, "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.False(result.HasValue); + } + + [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.Value)); + target.Root = second; + target.Root = null; + + Assert.Equal(new[] { "foo", "bar", null }, 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..6ab8b88415 --- /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.Value)); + tcs.SetResult("foo"); + sync.ExecutePostedCallbacks(); + + Assert.Equal(new object[] { null, "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.Value)); + + 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.Value)); + tcs.SetResult(new Class2("foo")); + sync.ExecutePostedCallbacks(); + + Assert.Equal(new object[] { null, "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..97e0dc7741 --- /dev/null +++ b/tests/Perspex.Markup.Xaml.UnitTests/Binding/XamlBindingTests.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.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)); + } + + 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..1a682b28b1 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; @@ -141,6 +142,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..3eb660f8a8 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; @@ -173,6 +174,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..3c1612ee76 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 { @@ -81,5 +82,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..6b4d32d2ef 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 { @@ -87,5 +88,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(); + } } }