From 292a04d973d35b0de3e682a4377b32e5cd59343b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 27 Sep 2015 02:22:49 +0200 Subject: [PATCH 01/72] Renamed DataTemplate -> FuncDataTemplate. --- samples/TestApplication/GalleryStyle.cs | 2 +- samples/TestApplication/Program.cs | 2 +- src/Perspex.Controls/ContentControl.cs | 2 +- .../Generators/TreeItemContainerGenerator.cs | 2 +- src/Perspex.Controls/IContentControl.cs | 2 +- src/Perspex.Controls/Perspex.Controls.csproj | 4 ++-- .../Templates/DataTemplateExtensions.cs | 2 +- .../{DataTemplate.cs => FuncDataTemplate.cs} | 14 +++++++------- .../{DataTemplate`1.cs => FuncDataTemplate`1.cs} | 10 +++++----- src/Perspex.Controls/Templates/TreeDataTemplate.cs | 2 +- src/Perspex.Themes.Default/MenuItemStyle.cs | 4 ++-- .../ItemsControlTests.cs | 2 +- .../Perspex.Controls.UnitTests/TabControlTests.cs | 2 +- 13 files changed, 25 insertions(+), 25 deletions(-) rename src/Perspex.Controls/Templates/{DataTemplate.cs => FuncDataTemplate.cs} (80%) rename src/Perspex.Controls/Templates/{DataTemplate`1.cs => FuncDataTemplate`1.cs} (84%) 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 bfbb8a17ce..147a9b0f7c 100644 --- a/samples/TestApplication/Program.cs +++ b/samples/TestApplication/Program.cs @@ -401,7 +401,7 @@ namespace TestApplication Margin = new Thickness(10), DataTemplates = new DataTemplates { - new DataTemplate(x => + new FuncDataTemplate(x => new StackPanel { Gap = 4, 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..3dabba7aad 100644 --- a/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs @@ -192,7 +192,7 @@ namespace Perspex.Controls.Generators if (template == null) { - template = DataTemplate.Default; + template = FuncDataTemplate.Default; } var treeTemplate = template as ITreeDataTemplate; 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/Perspex.Controls.csproj b/src/Perspex.Controls/Perspex.Controls.csproj index 6bcd55b76c..91b85d45eb 100644 --- a/src/Perspex.Controls/Perspex.Controls.csproj +++ b/src/Perspex.Controls/Perspex.Controls.csproj @@ -70,7 +70,7 @@ - + @@ -100,7 +100,7 @@ - + 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/TreeDataTemplate.cs index e384aaf90d..fc99c20d53 100644 --- a/src/Perspex.Controls/Templates/TreeDataTemplate.cs +++ b/src/Perspex.Controls/Templates/TreeDataTemplate.cs @@ -10,7 +10,7 @@ namespace Perspex.Controls.Templates /// /// A template used to build hierachical data. /// - public class TreeDataTemplate : DataTemplate, ITreeDataTemplate + public class TreeDataTemplate : FuncDataTemplate, ITreeDataTemplate { private readonly Func _itemsSelector; 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.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 57ef725e04..e4385d1bd7 100644 --- a/tests/Perspex.Controls.UnitTests/TabControlTests.cs +++ b/tests/Perspex.Controls.UnitTests/TabControlTests.cs @@ -147,7 +147,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, }; From 1257178cc5621798c8e789e5fad85efda01f005c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 27 Sep 2015 02:24:05 +0200 Subject: [PATCH 02/72] Renamed XamlDataTemplate -> DataTemplate --- .../Templates/{XamlDataTemplate.cs => DataTemplate.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Markup/Perspex.Markup.Xaml/Templates/{XamlDataTemplate.cs => DataTemplate.cs} (100%) diff --git a/src/Markup/Perspex.Markup.Xaml/Templates/XamlDataTemplate.cs b/src/Markup/Perspex.Markup.Xaml/Templates/DataTemplate.cs similarity index 100% rename from src/Markup/Perspex.Markup.Xaml/Templates/XamlDataTemplate.cs rename to src/Markup/Perspex.Markup.Xaml/Templates/DataTemplate.cs From 328f4c95d69b9332577ec21e4e4916b02e84d769 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 27 Sep 2015 02:43:25 +0200 Subject: [PATCH 03/72] Trying to define DataTemplates in XAML. Getting a crash from OmniXAML. --- samples/XamlTestApplication/MainViewModel.cs | 52 ------------------- .../ViewModels/MainWindowViewModel.cs | 22 ++++++++ .../ViewModels/TestItem.cs | 17 ++++++ .../XamlTestApplication/Views/MainWindow.cs | 3 +- .../XamlTestApplication/Views/MainWindow.paml | 23 ++++---- .../XamlTestApplication.csproj | 3 +- .../Context/PerspexObjectAssembler.cs | 2 +- .../Context/PerspexWiringContext.cs | 2 +- src/Markup/Perspex.Markup.Xaml/OmniXAML | 2 +- .../Perspex.Markup.Xaml.csproj | 2 +- .../Templates/DataTemplate.cs | 2 +- 11 files changed, 57 insertions(+), 73 deletions(-) delete mode 100644 samples/XamlTestApplication/MainViewModel.cs create mode 100644 samples/XamlTestApplication/ViewModels/MainWindowViewModel.cs create mode 100644 samples/XamlTestApplication/ViewModels/TestItem.cs diff --git a/samples/XamlTestApplication/MainViewModel.cs b/samples/XamlTestApplication/MainViewModel.cs deleted file mode 100644 index 450c8f422f..0000000000 --- a/samples/XamlTestApplication/MainViewModel.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.Collections.Generic; -using ReactiveUI; - -namespace XamlTestApplication -{ - public class MainViewModel : ReactiveObject - { - private string _name; - - public MainViewModel() - { - Name = "Jos\u00E9 Manuel"; - People = new List - { - new Person("a little bit of Monica in my life"), - new Person("a little bit of Erica by my side"), - new Person("a little bit of Rita is all I need"), - new Person("a little bit of Tina is what I see"), - new Person("a little bit of Sandra in the sun"), - new Person("a little bit of Mary all night long"), - new Person("a little bit of Jessica here I am"), - }; - } - - public string Name - { - get { return _name; } - set { this.RaiseAndSetIfChanged(ref _name, value); } - } - - public List People { get; set; } - } - - public class Person - { - private string _name; - - public Person(string name) - { - _name = name; - } - - public string Name - { - get { return _name; } - set { _name = value; } - } - } -} \ No newline at end of file diff --git a/samples/XamlTestApplication/ViewModels/MainWindowViewModel.cs b/samples/XamlTestApplication/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000000..75664f8188 --- /dev/null +++ b/samples/XamlTestApplication/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,22 @@ +// 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")); + } + } + + public List Items { get; } + } +} diff --git a/samples/XamlTestApplication/ViewModels/TestItem.cs b/samples/XamlTestApplication/ViewModels/TestItem.cs new file mode 100644 index 0000000000..6b711be54f --- /dev/null +++ b/samples/XamlTestApplication/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/XamlTestApplication/Views/MainWindow.cs b/samples/XamlTestApplication/Views/MainWindow.cs index dc218641bf..fe2dd3d347 100644 --- a/samples/XamlTestApplication/Views/MainWindow.cs +++ b/samples/XamlTestApplication/Views/MainWindow.cs @@ -10,6 +10,7 @@ using OmniXaml; using Perspex.Controls; using Perspex.Diagnostics; using Perspex.Markup.Xaml; +using XamlTestApplication.ViewModels; namespace XamlTestApplication.Views { @@ -18,7 +19,7 @@ namespace XamlTestApplication.Views public MainWindow() { InitializeComponent(); - + DataContext = new MainWindowViewModel(); DevTools.Attach(this); } diff --git a/samples/XamlTestApplication/Views/MainWindow.paml b/samples/XamlTestApplication/Views/MainWindow.paml index 0f37faa54c..baa5c9d5dd 100644 --- a/samples/XamlTestApplication/Views/MainWindow.paml +++ b/samples/XamlTestApplication/Views/MainWindow.paml @@ -1,6 +1,7 @@  @@ -49,20 +50,14 @@ - - - - - - - - - - - - - - + + + + + + + + diff --git a/samples/XamlTestApplication/XamlTestApplication.csproj b/samples/XamlTestApplication/XamlTestApplication.csproj index d70e2e3297..c5b09898a6 100644 --- a/samples/XamlTestApplication/XamlTestApplication.csproj +++ b/samples/XamlTestApplication/XamlTestApplication.csproj @@ -77,9 +77,10 @@ - + + diff --git a/src/Markup/Perspex.Markup.Xaml/Context/PerspexObjectAssembler.cs b/src/Markup/Perspex.Markup.Xaml/Context/PerspexObjectAssembler.cs index b51aae5969..06d78ab753 100644 --- a/src/Markup/Perspex.Markup.Xaml/Context/PerspexObjectAssembler.cs +++ b/src/Markup/Perspex.Markup.Xaml/Context/PerspexObjectAssembler.cs @@ -15,7 +15,7 @@ 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()); var assembler = new ObjectAssembler(wiringContext, new TopDownValueContext(), objectAssemblerSettings); _objectAssembler = new TemplateHostingObjectAssembler(assembler, mapping); diff --git a/src/Markup/Perspex.Markup.Xaml/Context/PerspexWiringContext.cs b/src/Markup/Perspex.Markup.Xaml/Context/PerspexWiringContext.cs index b2ab8c06ae..e5af8d5d4c 100644 --- a/src/Markup/Perspex.Markup.Xaml/Context/PerspexWiringContext.cs +++ b/src/Markup/Perspex.Markup.Xaml/Context/PerspexWiringContext.cs @@ -107,7 +107,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(DataTemplate), "Content"), }; contentPropertyProvider.AddAll(contentProperties); diff --git a/src/Markup/Perspex.Markup.Xaml/OmniXAML b/src/Markup/Perspex.Markup.Xaml/OmniXAML index 1461be589a..1aa1e5080b 160000 --- a/src/Markup/Perspex.Markup.Xaml/OmniXAML +++ b/src/Markup/Perspex.Markup.Xaml/OmniXAML @@ -1 +1 @@ -Subproject commit 1461be589a5d39d378fd177648c61a780271c742 +Subproject commit 1aa1e5080b04ccf40baef4420ee29ffef9303811 diff --git a/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj b/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj index 2bce8946ac..e2374ef20b 100644 --- a/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj +++ b/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj @@ -242,7 +242,7 @@ - + diff --git a/src/Markup/Perspex.Markup.Xaml/Templates/DataTemplate.cs b/src/Markup/Perspex.Markup.Xaml/Templates/DataTemplate.cs index 49e5ce48a2..e0b51e89f0 100644 --- a/src/Markup/Perspex.Markup.Xaml/Templates/DataTemplate.cs +++ b/src/Markup/Perspex.Markup.Xaml/Templates/DataTemplate.cs @@ -9,7 +9,7 @@ using Perspex.Controls.Templates; namespace Perspex.Markup.Xaml.Templates { [ContentProperty("Content")] - public class XamlDataTemplate : IDataTemplate + public class DataTemplate : IDataTemplate { private bool MyMatch(object data) { From 7ae7f56a280a725d2be10e36dfaea6f60193f5b7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 29 Sep 2015 22:37:16 +0200 Subject: [PATCH 04/72] Updated OmniXAML. --- src/Markup/Perspex.Markup.Xaml/OmniXAML | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Markup/Perspex.Markup.Xaml/OmniXAML b/src/Markup/Perspex.Markup.Xaml/OmniXAML index 1aa1e5080b..49e6ec001f 160000 --- a/src/Markup/Perspex.Markup.Xaml/OmniXAML +++ b/src/Markup/Perspex.Markup.Xaml/OmniXAML @@ -1 +1 @@ -Subproject commit 1aa1e5080b04ccf40baef4420ee29ffef9303811 +Subproject commit 49e6ec001f5873cf2290e0bc1f6f06ca9b9cf808 From 9a207b072ad1b2bbdd0f6747a7e3d0673fb30a5b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 29 Sep 2015 22:38:19 +0200 Subject: [PATCH 05/72] Added missing OmniXAML file. --- src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj b/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj index e2374ef20b..1f0e96020d 100644 --- a/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj +++ b/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj @@ -58,6 +58,7 @@ + From a0509caede3283d1e4b40a526c3c818e8aa672a4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 29 Sep 2015 23:34:10 +0200 Subject: [PATCH 06/72] Renamed TreeDataTemplate -> FuncTreeDataTemplate. --- samples/TestApplication/Program.cs | 2 +- samples/XamlTestApplication/Program.cs | 2 +- .../Generators/TreeItemContainerGenerator.cs | 2 +- src/Perspex.Controls/Perspex.Controls.csproj | 4 ++-- ...DataTemplate.cs => FuncTreeDataTemplate.cs} | 18 +++++++++--------- ...Template`1.cs => FuncTreeDataTemplate`1.cs} | 18 +++++++++--------- .../Views/LogicalTreeView.cs | 2 +- .../Views/VisualTreeView.cs | 2 +- 8 files changed, 25 insertions(+), 25 deletions(-) rename src/Perspex.Controls/Templates/{TreeDataTemplate.cs => FuncTreeDataTemplate.cs} (89%) rename src/Perspex.Controls/Templates/{TreeDataTemplate`1.cs => FuncTreeDataTemplate`1.cs} (88%) diff --git a/samples/TestApplication/Program.cs b/samples/TestApplication/Program.cs index 147a9b0f7c..ce9e4a540d 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), diff --git a/samples/XamlTestApplication/Program.cs b/samples/XamlTestApplication/Program.cs index d115b67809..c79fa68899 100644 --- a/samples/XamlTestApplication/Program.cs +++ b/samples/XamlTestApplication/Program.cs @@ -40,7 +40,7 @@ namespace XamlTestApplication { DataTemplates = new DataTemplates { - new TreeDataTemplate( + new FuncTreeDataTemplate( x => new TextBlock { Text = x.Name }, x => x.Children, x => true), diff --git a/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs b/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs index 3dabba7aad..4f35084d70 100644 --- a/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs @@ -199,7 +199,7 @@ namespace Perspex.Controls.Generators 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/Perspex.Controls.csproj b/src/Perspex.Controls/Perspex.Controls.csproj index 91b85d45eb..9085848a74 100644 --- a/src/Perspex.Controls/Perspex.Controls.csproj +++ b/src/Perspex.Controls/Perspex.Controls.csproj @@ -127,7 +127,7 @@ - + @@ -154,7 +154,7 @@ - + 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 fc99c20d53..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 : FuncDataTemplate, 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.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), }), From 1020f8c9b3de5ba8b75b52c506c4de3320661712 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 29 Sep 2015 23:36:58 +0200 Subject: [PATCH 07/72] Tidied up DataTemplate. --- .../Templates/DataTemplate.cs | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/Markup/Perspex.Markup.Xaml/Templates/DataTemplate.cs b/src/Markup/Perspex.Markup.Xaml/Templates/DataTemplate.cs index e0b51e89f0..9b1bf4731c 100644 --- a/src/Markup/Perspex.Markup.Xaml/Templates/DataTemplate.cs +++ b/src/Markup/Perspex.Markup.Xaml/Templates/DataTemplate.cs @@ -11,35 +11,30 @@ namespace Perspex.Markup.Xaml.Templates [ContentProperty("Content")] 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) - { - 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) + private Control CreateVisualTreeForItem(object data) { - return MyMatch(data); + var visualTreeForItem = Content.Load(); + visualTreeForItem.DataContext = data; + return visualTreeForItem; } } } \ No newline at end of file From d581a1785d3c744b6f12f62f06b8d6e25de20401 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 29 Sep 2015 23:37:33 +0200 Subject: [PATCH 08/72] Fixed test DataTemplate. --- samples/XamlTestApplication/Views/MainWindow.paml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/samples/XamlTestApplication/Views/MainWindow.paml b/samples/XamlTestApplication/Views/MainWindow.paml index baa5c9d5dd..b94dda826f 100644 --- a/samples/XamlTestApplication/Views/MainWindow.paml +++ b/samples/XamlTestApplication/Views/MainWindow.paml @@ -53,8 +53,10 @@ - - + + + + From 5cd27f7e8ffbabc1db6cdb017a8e8dc7ffe3438d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 29 Sep 2015 23:56:31 +0200 Subject: [PATCH 09/72] Started implementing TreeDataTemplate. Binding system needs to be updated first. --- .../ViewModels/MainWindowViewModel.cs | 36 +++++++++++++ .../XamlTestApplication/Views/MainWindow.paml | 26 +++++++--- samples/XamlTestApplication/Views/TestNode.cs | 14 +++++ .../XamlTestApplication.csproj | 1 + .../Perspex.Markup.Xaml.csproj | 1 + .../Templates/DataTemplate.cs | 8 +-- .../Templates/TreeDataTemplate.cs | 52 +++++++++++++++++++ 7 files changed, 123 insertions(+), 15 deletions(-) create mode 100644 samples/XamlTestApplication/Views/TestNode.cs create mode 100644 src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs diff --git a/samples/XamlTestApplication/ViewModels/MainWindowViewModel.cs b/samples/XamlTestApplication/ViewModels/MainWindowViewModel.cs index 75664f8188..1523299d41 100644 --- a/samples/XamlTestApplication/ViewModels/MainWindowViewModel.cs +++ b/samples/XamlTestApplication/ViewModels/MainWindowViewModel.cs @@ -15,8 +15,44 @@ namespace XamlTestApplication.ViewModels { 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/XamlTestApplication/Views/MainWindow.paml b/samples/XamlTestApplication/Views/MainWindow.paml index b94dda826f..7fcf6bf749 100644 --- a/samples/XamlTestApplication/Views/MainWindow.paml +++ b/samples/XamlTestApplication/Views/MainWindow.paml @@ -60,16 +60,26 @@ - + + + + + + + + + + + + + - - - - - - + + - + + + diff --git a/samples/XamlTestApplication/Views/TestNode.cs b/samples/XamlTestApplication/Views/TestNode.cs new file mode 100644 index 0000000000..94c8fb6052 --- /dev/null +++ b/samples/XamlTestApplication/Views/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; } + } +} diff --git a/samples/XamlTestApplication/XamlTestApplication.csproj b/samples/XamlTestApplication/XamlTestApplication.csproj index c5b09898a6..28a5382d7e 100644 --- a/samples/XamlTestApplication/XamlTestApplication.csproj +++ b/samples/XamlTestApplication/XamlTestApplication.csproj @@ -80,6 +80,7 @@ + diff --git a/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj b/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj index 1f0e96020d..06eb0f4792 100644 --- a/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj +++ b/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj @@ -245,6 +245,7 @@ + diff --git a/src/Markup/Perspex.Markup.Xaml/Templates/DataTemplate.cs b/src/Markup/Perspex.Markup.Xaml/Templates/DataTemplate.cs index 9b1bf4731c..aa07179dac 100644 --- a/src/Markup/Perspex.Markup.Xaml/Templates/DataTemplate.cs +++ b/src/Markup/Perspex.Markup.Xaml/Templates/DataTemplate.cs @@ -12,7 +12,6 @@ namespace Perspex.Markup.Xaml.Templates public class DataTemplate : IDataTemplate { public Type DataType { get; set; } - public TemplateContent Content { get; set; } public bool Match(object data) @@ -25,12 +24,7 @@ namespace Perspex.Markup.Xaml.Templates return DataType == data.GetType(); } - public IControl Build(object param) - { - return CreateVisualTreeForItem(param); - } - - private Control CreateVisualTreeForItem(object data) + public IControl Build(object data) { var visualTreeForItem = Content.Load(); visualTreeForItem.DataContext = data; 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..e66d12ba75 --- /dev/null +++ b/src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs @@ -0,0 +1,52 @@ +// 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 OmniXaml.Attributes; +using Perspex.Controls; +using Perspex.Controls.Templates; +using Perspex.Markup.Xaml.DataBinding; + +namespace Perspex.Markup.Xaml.Templates +{ + [ContentProperty("Content")] + public class TreeDataTemplate : ITreeDataTemplate + { + public Type DataType { get; set; } + public TemplateContent Content { get; set; } + public XamlBinding 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) + { + // TODO: Get value of ItemsSource here. + } + + 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 From 005cd231cc3b4aaec9e65321b0a49f3f8dd127fc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 30 Sep 2015 02:37:31 +0200 Subject: [PATCH 10/72] Initial implementation of ExpressionObserver. --- Perspex.sln | 14 ++ Perspex.v2.ncrunchsolution | Bin 1836 -> 2342 bytes .../Perspex.Markup/Binding/ExpressionNode.cs | 103 +++++++++++ .../Binding/ExpressionNodeBuilder.cs | 74 ++++++++ .../Binding/ExpressionObserver.cs | 55 ++++++ .../Perspex.Markup/Binding/ExpressionValue.cs | 26 +++ .../Binding/PropertyAccessorNode.cs | 67 +++++++ .../Perspex.Markup/Perspex.Markup.csproj | 93 ++++++++++ .../Perspex.Markup/Properties/AssemblyInfo.cs | 30 ++++ src/Markup/Perspex.Markup/packages.config | 13 ++ .../Binding/ExpressionNodeBuilderTests.cs | 29 ++++ .../Binding/ExpressionObserverTests.cs | 163 ++++++++++++++++++ .../Binding/NotifyingBase.cs | 38 ++++ .../Perspex.Markup.UnitTests.csproj | 103 +++++++++++ .../Properties/AssemblyInfo.cs | 36 ++++ .../Perspex.Markup.UnitTests/packages.config | 14 ++ 16 files changed, 858 insertions(+) create mode 100644 src/Markup/Perspex.Markup/Binding/ExpressionNode.cs create mode 100644 src/Markup/Perspex.Markup/Binding/ExpressionNodeBuilder.cs create mode 100644 src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs create mode 100644 src/Markup/Perspex.Markup/Binding/ExpressionValue.cs create mode 100644 src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs create mode 100644 src/Markup/Perspex.Markup/Perspex.Markup.csproj create mode 100644 src/Markup/Perspex.Markup/Properties/AssemblyInfo.cs create mode 100644 src/Markup/Perspex.Markup/packages.config create mode 100644 tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests.cs create mode 100644 tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests.cs create mode 100644 tests/Perspex.Markup.UnitTests/Binding/NotifyingBase.cs create mode 100644 tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj create mode 100644 tests/Perspex.Markup.UnitTests/Properties/AssemblyInfo.cs create mode 100644 tests/Perspex.Markup.UnitTests/packages.config diff --git a/Perspex.sln b/Perspex.sln index 22b196d2c8..3b97ab21cf 100644 --- a/Perspex.sln +++ b/Perspex.sln @@ -96,6 +96,10 @@ 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 Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\PlatformSupport\PlatformSupport.projitems*{e4d9629c-f168-4224-3f51-a5e482ffbc42}*SharedItemsImports = 13 @@ -235,6 +239,14 @@ 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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -260,5 +272,7 @@ 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} EndGlobalSection EndGlobal diff --git a/Perspex.v2.ncrunchsolution b/Perspex.v2.ncrunchsolution index fe4feb2cb68b687b4720bc1a47cbec77763ab35f..56e7a57022a3819f4a5ae943d50d65dc44d2efa5 100644 GIT binary patch literal 2342 zcmds(O>fgc5Qg7#CH@0vq=pg+35m*5R7i!?R23~B2Va8gHnfh7ybc9^Jn+nH9j8@$ z2u?1@+V$>y?7Tbkde?t`@9R{R3N6&=E2B~e-d`}gL|SXQ)FgVM9X;jQ*BE`FTpww* z!mV$j7wDcps_p#W5$VExPw=mF#W*9T0t!5t4{o4u;A!s88Vpn8u zLDp>V_mO>~LXBov&A>4CJQe!xcpt;InYBCAW6zb<>jtc~uBoiCuAl|7*fQ{p%6`LY z>{iEEm5x4fCggg9zQlfxG=md1HZ~Xw!!7@;Dt#jLMSbX)Y-g2P+^6g??sAi;??&p2 zd%}5OxzDzYdNs}_Ft=RYZmtb=7pX7q&r4U0|1kv!M_4LFqCVokyx?JOkQmcr?ZHn*P##QcZ}G@#R-|SDe@lmUiFR%J;wH z!HVP;db?!?+%K{1X_q@7XN4)UT@AIz`z8G=-a{ldPtmrgL%T*wv_xm3_E6F$Z@4uFtEElM}B@?OX|dmqz}ze;G$;jDhig#G&izgZ^YjHsi@X9J2s8Rt13o diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs b/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs new file mode 100644 index 0000000000..6e21887dea --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs @@ -0,0 +1,103 @@ +// 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.Subjects; + +namespace Perspex.Markup.Binding +{ + public 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) + { + Subscribe(_target); + } + else + { + CurrentValue = ExpressionValue.None; + } + + if (Next != null) + { + Next.Target = CurrentValue.Value; + } + } + } + + public ExpressionValue CurrentValue + { + get + { + return _value; + } + + set + { + if (value == null) + { + throw new ArgumentNullException("value"); + } + + _value = value; + + if (Next != null) + { + Next.Target = value.Value; + } + + if (_subject != null) + { + _subject.OnNext(value); + } + } + } + + public 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 abstract void Subscribe(object target); + + protected abstract 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..ebd4b85baf --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/ExpressionNodeBuilder.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Perspex.Markup.Binding +{ + public class ExpressionNodeBuilder + { + public static IList Build(string expression) + { + if (string.IsNullOrWhiteSpace(expression)) + { + throw new ArgumentException("'expression' may not be empty."); + } + + var syntaxTree = CSharpSyntaxTree.ParseText(expression, new CSharpParseOptions(kind: SourceCodeKind.Interactive)); + var syntaxRoot = syntaxTree.GetRoot(); + var syntax = syntaxRoot.ChildNodes().SingleOrDefault()?.ChildNodes()?.SingleOrDefault() as ExpressionStatementSyntax; + + if (syntax != null) + { + var result = new List(); + + foreach (SyntaxNode node in syntax.ChildNodes()) + { + var identifier = node as IdentifierNameSyntax; + var memberAccess = node as MemberAccessExpressionSyntax; + + if (identifier != null) + { + result.Add(new PropertyAccessorNode(identifier.Identifier.ValueText)); + } + else if (memberAccess != null) + { + Build(memberAccess, result); + } + } + + for (int i = 0; i < result.Count - 1; ++i) + { + result[i].Next = result[i + 1]; + } + + return result; + } + else + { + throw new Exception($"Invalid expression: {expression}"); + } + } + + private static void Build(MemberAccessExpressionSyntax syntax, IList result) + { + foreach (SyntaxNode node in syntax.ChildNodes()) + { + var identifier = node as IdentifierNameSyntax; + var memberAccess = node as MemberAccessExpressionSyntax; + + if (identifier != null) + { + result.Add(new PropertyAccessorNode(identifier.Identifier.ValueText)); + } + else if (memberAccess != null) + { + Build(memberAccess, result); + } + } + } + } +} diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs b/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs new file mode 100644 index 0000000000..1ddd23e372 --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs @@ -0,0 +1,55 @@ +// 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 System.Reactive; +using System.Reactive.Disposables; + +namespace Perspex.Markup.Binding +{ + public class ExpressionObserver : ObservableBase + { + private int _count; + + public ExpressionObserver(object root, string expression) + { + Root = root; + Nodes = ExpressionNodeBuilder.Build(expression); + } + + public object Root { get; } + + public IList Nodes { get; } + + protected override IDisposable SubscribeCore(IObserver observer) + { + IncrementCount(); + + var subscription = Nodes[0].Subscribe(observer); + + return Disposable.Create(() => + { + DecrementCount(); + subscription.Dispose(); + }); + } + + private void IncrementCount() + { + if (_count++ == 0) + { + Nodes[0].Target = Root; + } + } + + private void DecrementCount() + { + if (--_count == 0) + { + Nodes[0].Target = null; + } + } + } +} diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionValue.cs b/src/Markup/Perspex.Markup/Binding/ExpressionValue.cs new file mode 100644 index 0000000000..7579f98db4 --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/ExpressionValue.cs @@ -0,0 +1,26 @@ +// 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 +{ + public class ExpressionValue + { + public static readonly ExpressionValue None = new ExpressionValue(); + + public ExpressionValue(object value) + { + HasValue = true; + Value = value; + } + + private ExpressionValue() + { + HasValue = false; + } + + public bool HasValue { get; } + public object Value { get; } + } +} diff --git a/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs b/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs new file mode 100644 index 0000000000..c2ad61bbb4 --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.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.ComponentModel; +using System.Reflection; + +namespace Perspex.Markup.Binding +{ + public class PropertyAccessorNode : ExpressionNode + { + private PropertyInfo _propertyInfo; + + public PropertyAccessorNode(string propertyName) + { + PropertyName = propertyName; + } + + public string PropertyName { get; } + + protected override void Subscribe(object target) + { + var result = ExpressionValue.None; + + if (target != null) + { + _propertyInfo = target.GetType().GetTypeInfo().GetDeclaredProperty(PropertyName); + + if (_propertyInfo != null) + { + result = new ExpressionValue(_propertyInfo.GetValue(target)); + + var inpc = target as INotifyPropertyChanged; + + if (inpc != null) + { + inpc.PropertyChanged += PropertyChanged; + } + } + } + else + { + _propertyInfo = null; + } + + CurrentValue = result; + } + + private void PropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == PropertyName) + { + CurrentValue = new ExpressionValue(_propertyInfo.GetValue(Target)); + } + } + + protected override void Unsubscribe(object target) + { + var inpc = target as INotifyPropertyChanged; + + if (inpc != null) + { + inpc.PropertyChanged -= PropertyChanged; + } + } + } +} diff --git a/src/Markup/Perspex.Markup/Perspex.Markup.csproj b/src/Markup/Perspex.Markup/Perspex.Markup.csproj new file mode 100644 index 0000000000..5db8937f65 --- /dev/null +++ b/src/Markup/Perspex.Markup/Perspex.Markup.csproj @@ -0,0 +1,93 @@ + + + + + 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\Microsoft.CodeAnalysis.Common.1.1.0-beta1-20150812-01\lib\portable-net45+win8\Microsoft.CodeAnalysis.dll + True + + + ..\..\..\packages\Microsoft.CodeAnalysis.CSharp.1.1.0-beta1-20150812-01\lib\portable-net45+win8\Microsoft.CodeAnalysis.CSharp.dll + True + + + ..\..\..\packages\System.Collections.Immutable.1.1.36\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll + True + + + ..\..\..\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 + + + ..\..\..\packages\System.Reflection.Metadata.1.1.0-alpha-00009\lib\portable-net45+win8\System.Reflection.Metadata.dll + True + + + + + + + + + + + + \ 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..c3a8edb54e --- /dev/null +++ b/src/Markup/Perspex.Markup/Properties/AssemblyInfo.cs @@ -0,0 +1,30 @@ +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")] diff --git a/src/Markup/Perspex.Markup/packages.config b/src/Markup/Perspex.Markup/packages.config new file mode 100644 index 0000000000..aa2968fe54 --- /dev/null +++ b/src/Markup/Perspex.Markup/packages.config @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests.cs b/tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests.cs new file mode 100644 index 0000000000..cdbbc1d1d3 --- /dev/null +++ b/tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests.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 Perspex.Markup.Binding; +using Xunit; + +namespace Perspex.Markup.UnitTests.Binding +{ + public class ExpressionNodeBuilderTests + { + [Fact] + public void Should_Build_Single_Property() + { + var result = ExpressionNodeBuilder.Build("Foo"); + + Assert.Equal(1, result.Count); + Assert.IsType(result[0]); + } + + [Fact] + public void Should_Build_Property_Chain() + { + var result = ExpressionNodeBuilder.Build("Foo.Bar.Baz"); + + Assert.Equal(3, result.Count); + Assert.IsType(result[0]); + } + } +} diff --git a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests.cs b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests.cs new file mode 100644 index 0000000000..22770868b5 --- /dev/null +++ b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests.cs @@ -0,0 +1,163 @@ +// 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 + { + [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_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 { Class2 = new Class2 { Bar = "bar" } }; + var target = new ExpressionObserver(data, "Class2.Bar"); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x.Value)); + data.Class2.Bar = "baz"; + + Assert.Equal(new[] { "bar", "baz" }, result); + + sub.Dispose(); + + Assert.Equal(0, data.SubscriptionCount); + Assert.Equal(0, data.Class2.SubscriptionCount); + } + + [Fact] + public void Should_Track_Middle_Of_Property_Chain_Changing() + { + var data = new Class1 { Class2 = new Class2 { Bar = "bar" } }; + var target = new ExpressionObserver(data, "Class2.Bar"); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x.Value)); + var old = data.Class2; + data.Class2 = new Class2 { Bar = "baz" }; + + Assert.Equal(new[] { "bar", "baz" }, result); + + sub.Dispose(); + + Assert.Equal(0, data.SubscriptionCount); + Assert.Equal(0, data.Class2.SubscriptionCount); + Assert.Equal(0, old.SubscriptionCount); + } + + [Fact] + public void Should_Track_Middle_Of_Property_Chain_Breaking_Then_Mending() + { + var data = new Class1 { Class2 = new Class2 { Bar = "bar" } }; + var target = new ExpressionObserver(data, "Class2.Bar"); + var result = new List(); + + var sub = target.Subscribe(x => result.Add(x.Value)); + var old = data.Class2; + data.Class2 = null; + data.Class2 = new Class2 { Bar = "baz" }; + + Assert.Equal(new[] { "bar", null, "baz" }, result); + + sub.Dispose(); + + Assert.Equal(0, data.SubscriptionCount); + Assert.Equal(0, data.Class2.SubscriptionCount); + Assert.Equal(0, old.SubscriptionCount); + } + + private class Class1 : NotifyingBase + { + private string _foo; + private Class2 _class2; + + public string Foo + { + get { return _foo; } + set + { + _foo = value; + RaisePropertyChanged(nameof(Foo)); + } + } + + public Class2 Class2 + { + get { return _class2; } + set + { + _class2 = value; + RaisePropertyChanged(nameof(Class2)); + } + } + } + + private class Class2 : NotifyingBase + { + private string _bar; + + public string Bar + { + get { return _bar; } + set + { + _bar = value; + RaisePropertyChanged(nameof(Bar)); + } + } + } + } +} diff --git a/tests/Perspex.Markup.UnitTests/Binding/NotifyingBase.cs b/tests/Perspex.Markup.UnitTests/Binding/NotifyingBase.cs new file mode 100644 index 0000000000..ba8cfc57f7 --- /dev/null +++ b/tests/Perspex.Markup.UnitTests/Binding/NotifyingBase.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.ComponentModel; + +namespace Perspex.Markup.UnitTests.Binding +{ + public class NotifyingBase : INotifyPropertyChanged + { + private PropertyChangedEventHandler _propertyChanged; + + public event PropertyChangedEventHandler PropertyChanged + { + add + { + _propertyChanged += value; + ++SubscriptionCount; + } + + remove + { + _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..f1c7abc376 --- /dev/null +++ b/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj @@ -0,0 +1,103 @@ + + + + + + + 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 + + + + + + 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/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 From cf3834755dbaac17683bc9517514f7c6bbbcf79a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 30 Sep 2015 02:51:21 +0200 Subject: [PATCH 11/72] More ExpressionObserver tests. --- .../Perspex.Markup/Binding/ExpressionNode.cs | 4 +- .../Binding/PropertyAccessorNode.cs | 2 +- .../Binding/ExpressionObserverTests.cs | 78 +++++++++++++------ .../Binding/NotifyingBase.cs | 8 +- .../Perspex.Markup.UnitTests.csproj | 1 + 5 files changed, 65 insertions(+), 28 deletions(-) diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs b/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs index 6e21887dea..64fbe20873 100644 --- a/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs +++ b/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs @@ -35,7 +35,7 @@ namespace Perspex.Markup.Binding if (_target != null) { - Subscribe(_target); + SubscribeAndUpdate(_target); } else { @@ -95,7 +95,7 @@ namespace Perspex.Markup.Binding } } - protected abstract void Subscribe(object target); + protected abstract void SubscribeAndUpdate(object target); protected abstract void Unsubscribe(object target); diff --git a/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs b/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs index c2ad61bbb4..1c48d272d8 100644 --- a/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs +++ b/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs @@ -18,7 +18,7 @@ namespace Perspex.Markup.Binding public string PropertyName { get; } - protected override void Subscribe(object target) + protected override void SubscribeAndUpdate(object target) { var result = ExpressionValue.None; diff --git a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests.cs b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests.cs index 22770868b5..f564788f56 100644 --- a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests.cs +++ b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests.cs @@ -63,66 +63,94 @@ namespace Perspex.Markup.UnitTests.Binding [Fact] public void Should_Track_End_Of_Property_Chain_Changing() { - var data = new Class1 { Class2 = new Class2 { Bar = "bar" } }; - var target = new ExpressionObserver(data, "Class2.Bar"); + 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)); - data.Class2.Bar = "baz"; + ((Class2)data.Next).Bar = "baz"; Assert.Equal(new[] { "bar", "baz" }, result); sub.Dispose(); Assert.Equal(0, data.SubscriptionCount); - Assert.Equal(0, data.Class2.SubscriptionCount); + Assert.Equal(0, data.Next.SubscriptionCount); } [Fact] - public void Should_Track_Middle_Of_Property_Chain_Changing() + public void Should_Track_Property_Chain_Changing() { - var data = new Class1 { Class2 = new Class2 { Bar = "bar" } }; - var target = new ExpressionObserver(data, "Class2.Bar"); + 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.Class2; - data.Class2 = new Class2 { Bar = "baz" }; + 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.Class2.SubscriptionCount); + Assert.Equal(0, data.Next.SubscriptionCount); Assert.Equal(0, old.SubscriptionCount); } [Fact] - public void Should_Track_Middle_Of_Property_Chain_Breaking_Then_Mending() + public void Should_Track_Property_Chain_Breaking_With_Null_Then_Mending() { - var data = new Class1 { Class2 = new Class2 { Bar = "bar" } }; - var target = new ExpressionObserver(data, "Class2.Bar"); + 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.Class2; - data.Class2 = null; - data.Class2 = new Class2 { Bar = "baz" }; + 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.Class2.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); + } + + private interface INext + { + int SubscriptionCount { get; } + } + private class Class1 : NotifyingBase { private string _foo; - private Class2 _class2; + private INext _next; public string Foo { @@ -134,18 +162,18 @@ namespace Perspex.Markup.UnitTests.Binding } } - public Class2 Class2 + public INext Next { - get { return _class2; } + get { return _next; } set { - _class2 = value; - RaisePropertyChanged(nameof(Class2)); + _next = value; + RaisePropertyChanged(nameof(Next)); } } } - private class Class2 : NotifyingBase + private class Class2 : NotifyingBase, INext { private string _bar; @@ -159,5 +187,9 @@ namespace Perspex.Markup.UnitTests.Binding } } } + + private class WithoutBar : NotifyingBase, INext + { + } } } diff --git a/tests/Perspex.Markup.UnitTests/Binding/NotifyingBase.cs b/tests/Perspex.Markup.UnitTests/Binding/NotifyingBase.cs index ba8cfc57f7..5cc6a32fea 100644 --- a/tests/Perspex.Markup.UnitTests/Binding/NotifyingBase.cs +++ b/tests/Perspex.Markup.UnitTests/Binding/NotifyingBase.cs @@ -2,6 +2,7 @@ // 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 { @@ -19,8 +20,11 @@ namespace Perspex.Markup.UnitTests.Binding remove { - _propertyChanged -= value; - --SubscriptionCount; + if (_propertyChanged?.GetInvocationList().Contains(value) == true) + { + _propertyChanged -= value; + --SubscriptionCount; + } } } diff --git a/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj b/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj index f1c7abc376..675e280df9 100644 --- a/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj +++ b/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj @@ -74,6 +74,7 @@ + From 6e264dccc446d36f2f3f25240b9a44e8288b1d8a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 30 Sep 2015 20:22:44 +0200 Subject: [PATCH 12/72] Allow setting values via ExpressionObserver. --- .../Binding/ExpressionObserver.cs | 20 ++++++++ .../Binding/PropertyAccessorNode.cs | 11 +++++ .../Binding/ExpressionObserverTests.cs | 47 +++++++++++++++++++ .../Perspex.Markup.UnitTests.csproj | 3 ++ 4 files changed, 81 insertions(+) diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs b/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs index 1ddd23e372..4bc0192c9f 100644 --- a/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs +++ b/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs @@ -19,6 +19,26 @@ namespace Perspex.Markup.Binding Nodes = ExpressionNodeBuilder.Build(expression); } + public bool SetValue(object value) + { + var last = Nodes.Last() as PropertyAccessorNode; + + if (last != null) + { + try + { + IncrementCount(); + return last.SetValue(value); + } + finally + { + DecrementCount(); + } + } + + return false; + } + public object Root { get; } public IList Nodes { get; } diff --git a/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs b/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs index 1c48d272d8..351879d1b4 100644 --- a/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs +++ b/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs @@ -16,6 +16,17 @@ namespace Perspex.Markup.Binding PropertyName = propertyName; } + public bool SetValue(object value) + { + if (_propertyInfo != null) + { + _propertyInfo.SetValue(Target, value); + return true; + } + + return false; + } + public string PropertyName { get; } protected override void SubscribeAndUpdate(object target) diff --git a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests.cs b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests.cs index f564788f56..79be969d4f 100644 --- a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests.cs +++ b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests.cs @@ -142,6 +142,53 @@ namespace Perspex.Markup.UnitTests.Binding 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)); + } + private interface INext { int SubscriptionCount { get; } diff --git a/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj b/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj index 675e280df9..909f67155f 100644 --- a/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj +++ b/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj @@ -86,6 +86,9 @@ Perspex.Markup + + + From 772937ad1543e339b55eb10cadaec0fc9ea1fa6e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 30 Sep 2015 20:29:35 +0200 Subject: [PATCH 13/72] Change ExpressionValue to struct. And add some docs. --- .../Perspex.Markup/Binding/ExpressionNode.cs | 5 ---- .../Binding/ExpressionObserver.cs | 23 ++++++++++++++++++ .../Perspex.Markup/Binding/ExpressionValue.cs | 24 ++++++++++++++----- 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs b/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs index 64fbe20873..aec90ee070 100644 --- a/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs +++ b/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs @@ -58,11 +58,6 @@ namespace Perspex.Markup.Binding set { - if (value == null) - { - throw new ArgumentNullException("value"); - } - _value = value; if (Next != null) diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs b/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs index 4bc0192c9f..cdbeddda0c 100644 --- a/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs +++ b/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs @@ -9,16 +9,32 @@ using System.Reactive.Disposables; namespace Perspex.Markup.Binding { + /// + /// Observes the value of an expression on a root object. + /// public class ExpressionObserver : ObservableBase { private int _count; + /// + /// Initializes a new instance of the class. + /// + /// The root object. + /// The expression. public ExpressionObserver(object root, string expression) { Root = root; Nodes = ExpressionNodeBuilder.Build(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) { var last = Nodes.Last() as PropertyAccessorNode; @@ -39,10 +55,17 @@ namespace Perspex.Markup.Binding return false; } + /// + /// Gets the root object that the expression is being observed on. + /// public object Root { get; } + /// + /// Gets a list of nodes representing the parts of the expression. + /// public IList Nodes { get; } + /// protected override IDisposable SubscribeCore(IObserver observer) { IncrementCount(); diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionValue.cs b/src/Markup/Perspex.Markup/Binding/ExpressionValue.cs index 7579f98db4..2441acda73 100644 --- a/src/Markup/Perspex.Markup/Binding/ExpressionValue.cs +++ b/src/Markup/Perspex.Markup/Binding/ExpressionValue.cs @@ -5,22 +5,34 @@ using System; namespace Perspex.Markup.Binding { - public class ExpressionValue + /// + /// 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; } - private ExpressionValue() - { - HasValue = false; - } - + /// + /// 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; } } } From 962c1ea01b35fb9c100a897d055a5e0a7fa1d44c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 30 Sep 2015 20:48:53 +0200 Subject: [PATCH 14/72] Pass SetValue through expression chain. --- .../Perspex.Markup/Binding/ExpressionNode.cs | 8 +++-- .../Binding/ExpressionNodeBuilder.cs | 12 ++++--- .../Binding/ExpressionObserver.cs | 34 +++++++------------ .../Binding/PropertyAccessorNode.cs | 23 ++++++++----- .../Perspex.Markup/Properties/AssemblyInfo.cs | 2 ++ .../Binding/ExpressionNodeBuilderTests.cs | 18 ++++++++-- 6 files changed, 58 insertions(+), 39 deletions(-) diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs b/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs index aec90ee070..114cd43b80 100644 --- a/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs +++ b/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs @@ -2,12 +2,11 @@ // 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.Subjects; namespace Perspex.Markup.Binding { - public abstract class ExpressionNode : IObservable + internal abstract class ExpressionNode : IObservable { private object _target; @@ -72,6 +71,11 @@ namespace Perspex.Markup.Binding } } + public virtual bool SetValue(object value) + { + return Next?.SetValue(value) ?? false; + } + public IDisposable Subscribe(IObserver observer) { if (Next != null) diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionNodeBuilder.cs b/src/Markup/Perspex.Markup/Binding/ExpressionNodeBuilder.cs index ebd4b85baf..7d493066c7 100644 --- a/src/Markup/Perspex.Markup/Binding/ExpressionNodeBuilder.cs +++ b/src/Markup/Perspex.Markup/Binding/ExpressionNodeBuilder.cs @@ -1,16 +1,18 @@ -using System; +// 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 System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Perspex.Markup.Binding { - public class ExpressionNodeBuilder + internal class ExpressionNodeBuilder { - public static IList Build(string expression) + public static ExpressionNode Build(string expression) { if (string.IsNullOrWhiteSpace(expression)) { @@ -45,7 +47,7 @@ namespace Perspex.Markup.Binding result[i].Next = result[i + 1]; } - return result; + return result[0]; } else { diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs b/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs index cdbeddda0c..bd0b08b7c3 100644 --- a/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs +++ b/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs @@ -15,6 +15,7 @@ namespace Perspex.Markup.Binding public class ExpressionObserver : ObservableBase { private int _count; + private ExpressionNode _node; /// /// Initializes a new instance of the class. @@ -24,7 +25,7 @@ namespace Perspex.Markup.Binding public ExpressionObserver(object root, string expression) { Root = root; - Nodes = ExpressionNodeBuilder.Build(expression); + _node = ExpressionNodeBuilder.Build(expression); } /// @@ -37,22 +38,16 @@ namespace Perspex.Markup.Binding /// public bool SetValue(object value) { - var last = Nodes.Last() as PropertyAccessorNode; + IncrementCount(); - if (last != null) + try { - try - { - IncrementCount(); - return last.SetValue(value); - } - finally - { - DecrementCount(); - } + return _node.SetValue(value); + } + finally + { + DecrementCount(); } - - return false; } /// @@ -60,17 +55,12 @@ namespace Perspex.Markup.Binding /// public object Root { get; } - /// - /// Gets a list of nodes representing the parts of the expression. - /// - public IList Nodes { get; } - /// protected override IDisposable SubscribeCore(IObserver observer) { IncrementCount(); - var subscription = Nodes[0].Subscribe(observer); + var subscription = _node.Subscribe(observer); return Disposable.Create(() => { @@ -83,7 +73,7 @@ namespace Perspex.Markup.Binding { if (_count++ == 0) { - Nodes[0].Target = Root; + _node.Target = Root; } } @@ -91,7 +81,7 @@ namespace Perspex.Markup.Binding { if (--_count == 0) { - Nodes[0].Target = null; + _node.Target = null; } } } diff --git a/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs b/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs index 351879d1b4..27e931a089 100644 --- a/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs +++ b/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs @@ -7,7 +7,7 @@ using System.Reflection; namespace Perspex.Markup.Binding { - public class PropertyAccessorNode : ExpressionNode + internal class PropertyAccessorNode : ExpressionNode { private PropertyInfo _propertyInfo; @@ -16,19 +16,26 @@ namespace Perspex.Markup.Binding PropertyName = propertyName; } - public bool SetValue(object value) + public string PropertyName { get; } + + public override bool SetValue(object value) { - if (_propertyInfo != null) + if (Next != null) { - _propertyInfo.SetValue(Target, value); - return true; + return Next.SetValue(value); } + else + { + if (_propertyInfo != null) + { + _propertyInfo.SetValue(Target, value); + return true; + } - return false; + return false; + } } - public string PropertyName { get; } - protected override void SubscribeAndUpdate(object target) { var result = ExpressionValue.None; diff --git a/src/Markup/Perspex.Markup/Properties/AssemblyInfo.cs b/src/Markup/Perspex.Markup/Properties/AssemblyInfo.cs index c3a8edb54e..9076f81754 100644 --- a/src/Markup/Perspex.Markup/Properties/AssemblyInfo.cs +++ b/src/Markup/Perspex.Markup/Properties/AssemblyInfo.cs @@ -28,3 +28,5 @@ using System.Runtime.InteropServices; // [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/tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests.cs b/tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests.cs index cdbbc1d1d3..443d18cdef 100644 --- a/tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests.cs +++ b/tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests.cs @@ -1,6 +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 System.Collections.Generic; using Perspex.Markup.Binding; using Xunit; @@ -11,7 +12,7 @@ namespace Perspex.Markup.UnitTests.Binding [Fact] public void Should_Build_Single_Property() { - var result = ExpressionNodeBuilder.Build("Foo"); + var result = ToList(ExpressionNodeBuilder.Build("Foo")); Assert.Equal(1, result.Count); Assert.IsType(result[0]); @@ -20,10 +21,23 @@ namespace Perspex.Markup.UnitTests.Binding [Fact] public void Should_Build_Property_Chain() { - var result = ExpressionNodeBuilder.Build("Foo.Bar.Baz"); + var result = ToList(ExpressionNodeBuilder.Build("Foo.Bar.Baz")); Assert.Equal(3, result.Count); Assert.IsType(result[0]); } + + private List ToList(ExpressionNode node) + { + var result = new List(); + + while (node != null) + { + result.Add(node); + node = node.Next; + } + + return result; + } } } From 774a9c0911e9e8d590f9e65cce44ce3431d376a7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 30 Sep 2015 22:05:59 +0200 Subject: [PATCH 15/72] Implemented binding negation. --- .../Perspex.Markup/Binding/ExpressionNode.cs | 9 ++- .../Binding/ExpressionNodeBuilder.cs | 67 ++++++++-------- .../Perspex.Markup/Binding/LogicalNotNode.cs | 49 ++++++++++++ .../Binding/PropertyAccessorNode.cs | 3 +- .../Perspex.Markup/Perspex.Markup.csproj | 1 + .../Binding/ExpressionNodeBuilderTests.cs | 27 +++++++ .../ExpressionObserverTests_Negation.cs | 77 +++++++++++++++++++ ...cs => ExpressionObserverTests_Property.cs} | 2 +- .../Perspex.Markup.UnitTests.csproj | 3 +- 9 files changed, 194 insertions(+), 44 deletions(-) create mode 100644 src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs create mode 100644 tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Negation.cs rename tests/Perspex.Markup.UnitTests/Binding/{ExpressionObserverTests.cs => ExpressionObserverTests_Property.cs} (99%) diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs b/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs index 114cd43b80..ff40507e7e 100644 --- a/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs +++ b/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs @@ -14,12 +14,13 @@ namespace Perspex.Markup.Binding private ExpressionValue _value = ExpressionValue.None; - public ExpressionNode Next + public ExpressionNode(ExpressionNode next) { - get; - set; + Next = next; } + public ExpressionNode Next { get; } + public object Target { get { return _target; } @@ -76,7 +77,7 @@ namespace Perspex.Markup.Binding return Next?.SetValue(value) ?? false; } - public IDisposable Subscribe(IObserver observer) + public virtual IDisposable Subscribe(IObserver observer) { if (Next != null) { diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionNodeBuilder.cs b/src/Markup/Perspex.Markup/Binding/ExpressionNodeBuilder.cs index 7d493066c7..b37892be4c 100644 --- a/src/Markup/Perspex.Markup/Binding/ExpressionNodeBuilder.cs +++ b/src/Markup/Perspex.Markup/Binding/ExpressionNodeBuilder.cs @@ -21,33 +21,11 @@ namespace Perspex.Markup.Binding var syntaxTree = CSharpSyntaxTree.ParseText(expression, new CSharpParseOptions(kind: SourceCodeKind.Interactive)); var syntaxRoot = syntaxTree.GetRoot(); - var syntax = syntaxRoot.ChildNodes().SingleOrDefault()?.ChildNodes()?.SingleOrDefault() as ExpressionStatementSyntax; + var syntax = syntaxRoot.ChildNodes().SingleOrDefault()?.ChildNodes()?.SingleOrDefault(); if (syntax != null) { - var result = new List(); - - foreach (SyntaxNode node in syntax.ChildNodes()) - { - var identifier = node as IdentifierNameSyntax; - var memberAccess = node as MemberAccessExpressionSyntax; - - if (identifier != null) - { - result.Add(new PropertyAccessorNode(identifier.Identifier.ValueText)); - } - else if (memberAccess != null) - { - Build(memberAccess, result); - } - } - - for (int i = 0; i < result.Count - 1; ++i) - { - result[i].Next = result[i + 1]; - } - - return result[0]; + return Build(expression, syntax, null); } else { @@ -55,22 +33,37 @@ namespace Perspex.Markup.Binding } } - private static void Build(MemberAccessExpressionSyntax syntax, IList result) + private static ExpressionNode Build(string expression, SyntaxNode syntax, ExpressionNode next) { - foreach (SyntaxNode node in syntax.ChildNodes()) - { - var identifier = node as IdentifierNameSyntax; - var memberAccess = node as MemberAccessExpressionSyntax; + var expressionStatement = syntax as ExpressionStatementSyntax; + var identifier = syntax as IdentifierNameSyntax; + var memberAccess = syntax as MemberAccessExpressionSyntax; + var unaryExpression = syntax as PrefixUnaryExpressionSyntax; - if (identifier != null) - { - result.Add(new PropertyAccessorNode(identifier.Identifier.ValueText)); - } - else if (memberAccess != null) - { - Build(memberAccess, result); - } + if (expressionStatement != null) + { + return Build(expression, expressionStatement.Expression, next); + } + else if (identifier != null) + { + next = new PropertyAccessorNode(next, identifier.Identifier.ValueText); } + else if (memberAccess != null) + { + next = new PropertyAccessorNode(next, memberAccess.Name.Identifier.ValueText); + next = Build(expression, memberAccess.Expression, next); + } + else if (unaryExpression != null && unaryExpression.Kind() == SyntaxKind.LogicalNotExpression) + { + next = Build(expression, unaryExpression.Operand, next); + next = new LogicalNotNode(next); + } + else + { + throw new Exception($"Invalid expression: {expression}"); + } + + return next; } } } diff --git a/src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs b/src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs new file mode 100644 index 0000000000..55244e28eb --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs @@ -0,0 +1,49 @@ +// 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 LogicalNotNode(ExpressionNode next) + : base(next) + { + } + + public override IDisposable Subscribe(IObserver observer) + { + return Next.Select(x => Negate(x)).Subscribe(observer); + } + + protected override void SubscribeAndUpdate(object target) + { + CurrentValue = new ExpressionValue(target); + } + + protected override void Unsubscribe(object target) + { + } + + 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/PropertyAccessorNode.cs b/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs index 27e931a089..da54212165 100644 --- a/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs +++ b/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs @@ -11,7 +11,8 @@ namespace Perspex.Markup.Binding { private PropertyInfo _propertyInfo; - public PropertyAccessorNode(string propertyName) + public PropertyAccessorNode(ExpressionNode next, string propertyName) + : base(next) { PropertyName = propertyName; } diff --git a/src/Markup/Perspex.Markup/Perspex.Markup.csproj b/src/Markup/Perspex.Markup/Perspex.Markup.csproj index 5db8937f65..fd08d6e577 100644 --- a/src/Markup/Perspex.Markup/Perspex.Markup.csproj +++ b/src/Markup/Perspex.Markup/Perspex.Markup.csproj @@ -36,6 +36,7 @@ + diff --git a/tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests.cs b/tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests.cs index 443d18cdef..a6759d4a1f 100644 --- a/tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests.cs +++ b/tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests.cs @@ -25,6 +25,33 @@ namespace Perspex.Markup.UnitTests.Binding Assert.Equal(3, result.Count); Assert.IsType(result[0]); + Assert.IsType(result[1]); + Assert.IsType(result[2]); + } + + [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]); + Assert.IsType(result[1]); + Assert.IsType(result[2]); + Assert.IsType(result[3]); + } + + [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]); + Assert.IsType(result[2]); + Assert.IsType(result[3]); + Assert.IsType(result[4]); } private List ToList(ExpressionNode node) 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..b38fb809bb --- /dev/null +++ b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Negation.cs @@ -0,0 +1,77 @@ +// 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.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_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); + } + } +} diff --git a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests.cs b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Property.cs similarity index 99% rename from tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests.cs rename to tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Property.cs index 79be969d4f..ccda547bd5 100644 --- a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests.cs +++ b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Property.cs @@ -9,7 +9,7 @@ using Xunit; namespace Perspex.Markup.UnitTests.Binding { - public class ExpressionObserverTests + public class ExpressionObserverTests_Property { [Fact] public async void Should_Get_Simple_Property_Value() diff --git a/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj b/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj index 909f67155f..04c42cba55 100644 --- a/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj +++ b/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj @@ -72,7 +72,8 @@ - + + From 7072f0a66c5c234ed67f7a86a8ff76e532bfc814 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 30 Sep 2015 23:42:19 +0200 Subject: [PATCH 16/72] Don't allow !ed expressions to be set. --- .../Perspex.Markup/Binding/LogicalNotNode.cs | 5 +++++ .../ExpressionObserverTests_Negation.cs | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs b/src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs index 55244e28eb..ca35ea5f12 100644 --- a/src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs +++ b/src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs @@ -14,6 +14,11 @@ namespace Perspex.Markup.Binding { } + 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); diff --git a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Negation.cs b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Negation.cs index b38fb809bb..614f27b34c 100644 --- a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Negation.cs +++ b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Negation.cs @@ -1,6 +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 System; using System.Reactive.Linq; using Perspex.Markup.Binding; using Xunit; @@ -64,6 +65,16 @@ namespace Perspex.Markup.UnitTests.Binding 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() { @@ -73,5 +84,14 @@ namespace Perspex.Markup.UnitTests.Binding 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")); + } } } From e3b6ce41863b61643a3485dfdb5c94e8be4ffecf Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 1 Oct 2015 01:08:37 +0200 Subject: [PATCH 17/72] Add support for indexers in binding expressions. --- .../Binding/ElementAccessorNode.cs | 107 +++++++++++++++++ .../Perspex.Markup/Binding/ExpressionNode.cs | 10 +- .../Binding/ExpressionNodeBuilder.cs | 36 ++++++ .../Perspex.Markup/Binding/LogicalNotNode.cs | 9 -- .../Binding/PropertyAccessorNode.cs | 16 +-- .../Perspex.Markup/Perspex.Markup.csproj | 1 + .../Binding/ExpressionNodeBuilderTests.cs | 70 +++++++++-- .../ExpressionObserverTests_Indexer.cs | 113 ++++++++++++++++++ .../Perspex.Markup.UnitTests.csproj | 1 + 9 files changed, 334 insertions(+), 29 deletions(-) create mode 100644 src/Markup/Perspex.Markup/Binding/ElementAccessorNode.cs create mode 100644 tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Indexer.cs diff --git a/src/Markup/Perspex.Markup/Binding/ElementAccessorNode.cs b/src/Markup/Perspex.Markup/Binding/ElementAccessorNode.cs new file mode 100644 index 0000000000..d8d4e9979c --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/ElementAccessorNode.cs @@ -0,0 +1,107 @@ +// 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 ElementAccessorNode : ExpressionNode + { + private int[] _intArgs; + + public ElementAccessorNode(ExpressionNode next, IList arguments) + : base(next) + { + 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/ExpressionNode.cs b/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs index ff40507e7e..4bb4356c30 100644 --- a/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs +++ b/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs @@ -95,9 +95,13 @@ namespace Perspex.Markup.Binding } } - protected abstract void SubscribeAndUpdate(object target); - - protected abstract void Unsubscribe(object target); + 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 index b37892be4c..abcba10452 100644 --- a/src/Markup/Perspex.Markup/Binding/ExpressionNodeBuilder.cs +++ b/src/Markup/Perspex.Markup/Binding/ExpressionNodeBuilder.cs @@ -39,6 +39,7 @@ namespace Perspex.Markup.Binding var identifier = syntax as IdentifierNameSyntax; var memberAccess = syntax as MemberAccessExpressionSyntax; var unaryExpression = syntax as PrefixUnaryExpressionSyntax; + var elementAccess = syntax as ElementAccessExpressionSyntax; if (expressionStatement != null) { @@ -58,6 +59,11 @@ namespace Perspex.Markup.Binding next = Build(expression, unaryExpression.Operand, next); next = new LogicalNotNode(next); } + else if (elementAccess != null) + { + next = Build(expression, elementAccess, next); + next = Build(expression, elementAccess.Expression, next); + } else { throw new Exception($"Invalid expression: {expression}"); @@ -65,5 +71,35 @@ namespace Perspex.Markup.Binding return next; } + + private static ExpressionNode Build(string expression, ElementAccessExpressionSyntax syntax, ExpressionNode next) + { + var argList = syntax.ArgumentList as BracketedArgumentListSyntax; + + if (argList != null) + { + var args = new List(); + + foreach (var arg in argList.Arguments) + { + var literal = arg.Expression as LiteralExpressionSyntax; + + if (literal != null) + { + args.Add(literal.Token.Value); + } + else + { + throw new Exception($"Invalid expression: {expression}"); + } + } + + return new ElementAccessorNode(next, args); + } + else + { + throw new Exception($"Invalid expression: {expression}"); + } + } } } diff --git a/src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs b/src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs index ca35ea5f12..6ea29deffa 100644 --- a/src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs +++ b/src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs @@ -24,15 +24,6 @@ namespace Perspex.Markup.Binding return Next.Select(x => Negate(x)).Subscribe(observer); } - protected override void SubscribeAndUpdate(object target) - { - CurrentValue = new ExpressionValue(target); - } - - protected override void Unsubscribe(object target) - { - } - private ExpressionValue Negate(ExpressionValue v) { if (v.HasValue) diff --git a/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs b/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs index da54212165..0a4b4b737e 100644 --- a/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs +++ b/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs @@ -65,14 +65,6 @@ namespace Perspex.Markup.Binding CurrentValue = result; } - private void PropertyChanged(object sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == PropertyName) - { - CurrentValue = new ExpressionValue(_propertyInfo.GetValue(Target)); - } - } - protected override void Unsubscribe(object target) { var inpc = target as INotifyPropertyChanged; @@ -82,5 +74,13 @@ namespace Perspex.Markup.Binding inpc.PropertyChanged -= PropertyChanged; } } + + private void PropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == PropertyName) + { + CurrentValue = new ExpressionValue(_propertyInfo.GetValue(Target)); + } + } } } diff --git a/src/Markup/Perspex.Markup/Perspex.Markup.csproj b/src/Markup/Perspex.Markup/Perspex.Markup.csproj index fd08d6e577..7cd19704b7 100644 --- a/src/Markup/Perspex.Markup/Perspex.Markup.csproj +++ b/src/Markup/Perspex.Markup/Perspex.Markup.csproj @@ -37,6 +37,7 @@ + diff --git a/tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests.cs b/tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests.cs index a6759d4a1f..18cd6c3578 100644 --- a/tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests.cs +++ b/tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests.cs @@ -2,6 +2,7 @@ // 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; @@ -24,9 +25,9 @@ namespace Perspex.Markup.UnitTests.Binding var result = ToList(ExpressionNodeBuilder.Build("Foo.Bar.Baz")); Assert.Equal(3, result.Count); - Assert.IsType(result[0]); - Assert.IsType(result[1]); - Assert.IsType(result[2]); + AssertIsProperty(result[0], "Foo"); + AssertIsProperty(result[1], "Bar"); + AssertIsProperty(result[2], "Baz"); } [Fact] @@ -36,9 +37,9 @@ namespace Perspex.Markup.UnitTests.Binding Assert.Equal(4, result.Count); Assert.IsType(result[0]); - Assert.IsType(result[1]); - Assert.IsType(result[2]); - Assert.IsType(result[3]); + AssertIsProperty(result[1], "Foo"); + AssertIsProperty(result[2], "Bar"); + AssertIsProperty(result[3], "Baz"); } [Fact] @@ -49,9 +50,60 @@ namespace Perspex.Markup.UnitTests.Binding Assert.Equal(5, result.Count); Assert.IsType(result[0]); Assert.IsType(result[1]); - Assert.IsType(result[2]); - Assert.IsType(result[3]); - Assert.IsType(result[4]); + 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[5]")); + + Assert.Equal(2, result.Count); + AssertIsProperty(result[0], "Foo"); + AssertIsIndexer(result[1], 5); + Assert.IsType(result[1]); + Assert.Equal(new object[] { 5 }, ((ElementAccessorNode)result[1]).Arguments.ToArray()); + } + + [Fact] + public void Should_Build_Multiple_Indexed_Property() + { + var result = ToList(ExpressionNodeBuilder.Build("Foo[5, 6]")); + + Assert.Equal(2, result.Count); + AssertIsProperty(result[0], "Foo"); + Assert.IsType(result[1]); + Assert.Equal(new object[] { 5, 6 }, ((ElementAccessorNode)result[1]).Arguments.ToArray()); + } + + [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"); + Assert.IsType(result[2]); + 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 = (ElementAccessorNode)node; + Assert.Equal(e.Arguments.ToArray(), args.ToArray()); } private List ToList(ExpressionNode node) 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/Perspex.Markup.UnitTests.csproj b/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj index 04c42cba55..eae2018e22 100644 --- a/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj +++ b/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj @@ -72,6 +72,7 @@ + From 0f5af3db6335a1d1b5e6a23453bd46e9d7d3f705 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 1 Oct 2015 01:08:37 +0200 Subject: [PATCH 18/72] Use Sprache for parsing expressions. Use sprache instead of Roslyn for parsing expressions. One test still failing. --- Perspex.v2.ncrunchsolution | Bin 2342 -> 1836 bytes .../Binding/ElementAccessorNode.cs | 107 +++++++++++++++++ .../Perspex.Markup/Binding/ExpressionNode.cs | 10 +- .../Binding/ExpressionNodeBuilder.cs | 51 ++++++-- .../Perspex.Markup/Binding/LogicalNotNode.cs | 9 -- .../Binding/PropertyAccessorNode.cs | 16 +-- .../CSharp/BracketedArgumentListSyntax.cs | 18 +++ .../Parsers/CSharp/CSharpSyntaxTree.cs | 17 +++ .../CSharp/ElementAccessExpressionSyntax.cs | 19 +++ .../CSharp/ExpressionStatementSyntax.cs | 15 +++ .../Parsers/CSharp/ExpressionSyntax.cs | 9 ++ .../CSharp/Grammar/ExpressionGrammar.cs | 64 ++++++++++ .../CSharp/Grammar/IdentifierGrammar.cs | 41 +++++++ .../Parsers/CSharp/Grammar/LiteralGrammar.cs | 20 ++++ .../Parsers/CSharp/IdentifierSyntax.cs | 17 +++ .../Parsers/CSharp/LiteralExpressionSyntax.cs | 17 +++ .../CSharp/MemberAccessExpressionSyntax.cs | 17 +++ .../CSharp/PrefixUnaryExpressionSyntax.cs | 23 ++++ .../Parsers/CSharp/SyntaxKind.cs | 10 ++ .../Parsers/CSharp/SyntaxNode.cs | 9 ++ .../Parsers/CSharp/SyntaxToken.cs | 15 +++ .../Perspex.Markup/Perspex.Markup.csproj | 34 +++--- src/Markup/Perspex.Markup/packages.config | 6 +- .../Binding/ExpressionNodeBuilderTests.cs | 81 +++++++++++-- .../ExpressionObserverTests_Indexer.cs | 113 ++++++++++++++++++ .../Perspex.Markup.UnitTests.csproj | 1 + 26 files changed, 677 insertions(+), 62 deletions(-) create mode 100644 src/Markup/Perspex.Markup/Binding/ElementAccessorNode.cs create mode 100644 src/Markup/Perspex.Markup/Parsers/CSharp/BracketedArgumentListSyntax.cs create mode 100644 src/Markup/Perspex.Markup/Parsers/CSharp/CSharpSyntaxTree.cs create mode 100644 src/Markup/Perspex.Markup/Parsers/CSharp/ElementAccessExpressionSyntax.cs create mode 100644 src/Markup/Perspex.Markup/Parsers/CSharp/ExpressionStatementSyntax.cs create mode 100644 src/Markup/Perspex.Markup/Parsers/CSharp/ExpressionSyntax.cs create mode 100644 src/Markup/Perspex.Markup/Parsers/CSharp/Grammar/ExpressionGrammar.cs create mode 100644 src/Markup/Perspex.Markup/Parsers/CSharp/Grammar/IdentifierGrammar.cs create mode 100644 src/Markup/Perspex.Markup/Parsers/CSharp/Grammar/LiteralGrammar.cs create mode 100644 src/Markup/Perspex.Markup/Parsers/CSharp/IdentifierSyntax.cs create mode 100644 src/Markup/Perspex.Markup/Parsers/CSharp/LiteralExpressionSyntax.cs create mode 100644 src/Markup/Perspex.Markup/Parsers/CSharp/MemberAccessExpressionSyntax.cs create mode 100644 src/Markup/Perspex.Markup/Parsers/CSharp/PrefixUnaryExpressionSyntax.cs create mode 100644 src/Markup/Perspex.Markup/Parsers/CSharp/SyntaxKind.cs create mode 100644 src/Markup/Perspex.Markup/Parsers/CSharp/SyntaxNode.cs create mode 100644 src/Markup/Perspex.Markup/Parsers/CSharp/SyntaxToken.cs create mode 100644 tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Indexer.cs diff --git a/Perspex.v2.ncrunchsolution b/Perspex.v2.ncrunchsolution index 56e7a57022a3819f4a5ae943d50d65dc44d2efa5..fe4feb2cb68b687b4720bc1a47cbec77763ab35f 100644 GIT binary patch delta 19 acmZ1`w1#g(3>&izgZ^YjHsi@X9J2s8Rt13o literal 2342 zcmds(O>fgc5Qg7#CH@0vq=pg+35m*5R7i!?R23~B2Va8gHnfh7ybc9^Jn+nH9j8@$ z2u?1@+V$>y?7Tbkde?t`@9R{R3N6&=E2B~e-d`}gL|SXQ)FgVM9X;jQ*BE`FTpww* z!mV$j7wDcps_p#W5$VExPw=mF#W*9T0t!5t4{o4u;A!s88Vpn8u zLDp>V_mO>~LXBov&A>4CJQe!xcpt;InYBCAW6zb<>jtc~uBoiCuAl|7*fQ{p%6`LY z>{iEEm5x4fCggg9zQlfxG=md1HZ~Xw!!7@;Dt#jLMSbX)Y-g2P+^6g??sAi;??&p2 zd%}5OxzDzYdNs}_Ft=RYZmtb=7pX7q&r4U0|1kv!M_4LFqCVokyx?JOkQmcr?ZHn*P##QcZ}G@#R-|SDe@lmUiFR%J;wH z!HVP;db?!?+%K{1X_q@7XN4)UT@AIz`z8G=-a{ldPtmrgL%T*wv_xm3_E6F$Z@4uFtEElM}B@?OX|dmqz}ze;G$;jDhig#G arguments) + : base(next) + { + 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/ExpressionNode.cs b/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs index ff40507e7e..4bb4356c30 100644 --- a/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs +++ b/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs @@ -95,9 +95,13 @@ namespace Perspex.Markup.Binding } } - protected abstract void SubscribeAndUpdate(object target); - - protected abstract void Unsubscribe(object target); + 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 index b37892be4c..4667bfbf13 100644 --- a/src/Markup/Perspex.Markup/Binding/ExpressionNodeBuilder.cs +++ b/src/Markup/Perspex.Markup/Binding/ExpressionNodeBuilder.cs @@ -3,10 +3,7 @@ using System; using System.Collections.Generic; -using System.Linq; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; +using Perspex.Markup.Parsers.CSharp; namespace Perspex.Markup.Binding { @@ -19,9 +16,7 @@ namespace Perspex.Markup.Binding throw new ArgumentException("'expression' may not be empty."); } - var syntaxTree = CSharpSyntaxTree.ParseText(expression, new CSharpParseOptions(kind: SourceCodeKind.Interactive)); - var syntaxRoot = syntaxTree.GetRoot(); - var syntax = syntaxRoot.ChildNodes().SingleOrDefault()?.ChildNodes()?.SingleOrDefault(); + var syntax = CSharpSyntaxTree.ParseExpression(expression); if (syntax != null) { @@ -36,9 +31,10 @@ namespace Perspex.Markup.Binding private static ExpressionNode Build(string expression, SyntaxNode syntax, ExpressionNode next) { var expressionStatement = syntax as ExpressionStatementSyntax; - var identifier = syntax as IdentifierNameSyntax; + var identifier = syntax as IdentifierSyntax; var memberAccess = syntax as MemberAccessExpressionSyntax; var unaryExpression = syntax as PrefixUnaryExpressionSyntax; + var elementAccess = syntax as ElementAccessExpressionSyntax; if (expressionStatement != null) { @@ -46,18 +42,23 @@ namespace Perspex.Markup.Binding } else if (identifier != null) { - next = new PropertyAccessorNode(next, identifier.Identifier.ValueText); + next = new PropertyAccessorNode(next, identifier.Name); } else if (memberAccess != null) { - next = new PropertyAccessorNode(next, memberAccess.Name.Identifier.ValueText); next = Build(expression, memberAccess.Expression, next); + next = new PropertyAccessorNode(next, memberAccess.Member.Name); } else if (unaryExpression != null && unaryExpression.Kind() == SyntaxKind.LogicalNotExpression) { next = Build(expression, unaryExpression.Operand, next); next = new LogicalNotNode(next); } + else if (elementAccess != null) + { + next = Build(expression, elementAccess, next); + next = Build(expression, elementAccess.Expression, next); + } else { throw new Exception($"Invalid expression: {expression}"); @@ -65,5 +66,35 @@ namespace Perspex.Markup.Binding return next; } + + private static ExpressionNode Build(string expression, ElementAccessExpressionSyntax syntax, ExpressionNode next) + { + var argList = syntax.ArgumentList as BracketedArgumentListSyntax; + + if (argList != null) + { + var args = new List(); + + foreach (var arg in argList.Arguments) + { + var literal = arg as LiteralExpressionSyntax; + + if (literal != null) + { + args.Add(literal.Value); + } + else + { + throw new Exception($"Invalid expression: {expression}"); + } + } + + return new ElementAccessorNode(next, args); + } + else + { + throw new Exception($"Invalid expression: {expression}"); + } + } } } diff --git a/src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs b/src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs index ca35ea5f12..6ea29deffa 100644 --- a/src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs +++ b/src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs @@ -24,15 +24,6 @@ namespace Perspex.Markup.Binding return Next.Select(x => Negate(x)).Subscribe(observer); } - protected override void SubscribeAndUpdate(object target) - { - CurrentValue = new ExpressionValue(target); - } - - protected override void Unsubscribe(object target) - { - } - private ExpressionValue Negate(ExpressionValue v) { if (v.HasValue) diff --git a/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs b/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs index da54212165..0a4b4b737e 100644 --- a/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs +++ b/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs @@ -65,14 +65,6 @@ namespace Perspex.Markup.Binding CurrentValue = result; } - private void PropertyChanged(object sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == PropertyName) - { - CurrentValue = new ExpressionValue(_propertyInfo.GetValue(Target)); - } - } - protected override void Unsubscribe(object target) { var inpc = target as INotifyPropertyChanged; @@ -82,5 +74,13 @@ namespace Perspex.Markup.Binding inpc.PropertyChanged -= PropertyChanged; } } + + private void PropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == PropertyName) + { + CurrentValue = new ExpressionValue(_propertyInfo.GetValue(Target)); + } + } } } diff --git a/src/Markup/Perspex.Markup/Parsers/CSharp/BracketedArgumentListSyntax.cs b/src/Markup/Perspex.Markup/Parsers/CSharp/BracketedArgumentListSyntax.cs new file mode 100644 index 0000000000..52a53313be --- /dev/null +++ b/src/Markup/Perspex.Markup/Parsers/CSharp/BracketedArgumentListSyntax.cs @@ -0,0 +1,18 @@ +// 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; + +namespace Perspex.Markup.Parsers.CSharp +{ + internal class BracketedArgumentListSyntax + { + public BracketedArgumentListSyntax(IEnumerable arguments) + { + Arguments = arguments.ToList(); + } + + public IList Arguments { get; } + } +} diff --git a/src/Markup/Perspex.Markup/Parsers/CSharp/CSharpSyntaxTree.cs b/src/Markup/Perspex.Markup/Parsers/CSharp/CSharpSyntaxTree.cs new file mode 100644 index 0000000000..19a6f8ac07 --- /dev/null +++ b/src/Markup/Perspex.Markup/Parsers/CSharp/CSharpSyntaxTree.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. + +using System; +using Perspex.Markup.Parsers.CSharp.Grammar; +using Sprache; + +namespace Perspex.Markup.Parsers.CSharp +{ + public class CSharpSyntaxTree + { + internal static ExpressionStatementSyntax ParseExpression(string expression) + { + return ExpressionGrammar.ExpressionStatement().Parse(expression); + } + } +} diff --git a/src/Markup/Perspex.Markup/Parsers/CSharp/ElementAccessExpressionSyntax.cs b/src/Markup/Perspex.Markup/Parsers/CSharp/ElementAccessExpressionSyntax.cs new file mode 100644 index 0000000000..6568e6f1a7 --- /dev/null +++ b/src/Markup/Perspex.Markup/Parsers/CSharp/ElementAccessExpressionSyntax.cs @@ -0,0 +1,19 @@ +// 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.Parsers.CSharp +{ + internal class ElementAccessExpressionSyntax : ExpressionSyntax + { + public ElementAccessExpressionSyntax( + ExpressionSyntax expression, + BracketedArgumentListSyntax argumentList) + { + Expression = expression; + ArgumentList = argumentList; + } + + public ExpressionSyntax Expression { get; } + public BracketedArgumentListSyntax ArgumentList { get; } + } +} diff --git a/src/Markup/Perspex.Markup/Parsers/CSharp/ExpressionStatementSyntax.cs b/src/Markup/Perspex.Markup/Parsers/CSharp/ExpressionStatementSyntax.cs new file mode 100644 index 0000000000..6c653eaea5 --- /dev/null +++ b/src/Markup/Perspex.Markup/Parsers/CSharp/ExpressionStatementSyntax.cs @@ -0,0 +1,15 @@ +// 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.Parsers.CSharp +{ + internal class ExpressionStatementSyntax : SyntaxNode + { + public ExpressionStatementSyntax(ExpressionSyntax expression) + { + Expression = expression; + } + + public ExpressionSyntax Expression { get; } + } +} diff --git a/src/Markup/Perspex.Markup/Parsers/CSharp/ExpressionSyntax.cs b/src/Markup/Perspex.Markup/Parsers/CSharp/ExpressionSyntax.cs new file mode 100644 index 0000000000..403bc7b6b6 --- /dev/null +++ b/src/Markup/Perspex.Markup/Parsers/CSharp/ExpressionSyntax.cs @@ -0,0 +1,9 @@ +// 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.Parsers.CSharp +{ + internal class ExpressionSyntax : SyntaxNode + { + } +} diff --git a/src/Markup/Perspex.Markup/Parsers/CSharp/Grammar/ExpressionGrammar.cs b/src/Markup/Perspex.Markup/Parsers/CSharp/Grammar/ExpressionGrammar.cs new file mode 100644 index 0000000000..71f847d312 --- /dev/null +++ b/src/Markup/Perspex.Markup/Parsers/CSharp/Grammar/ExpressionGrammar.cs @@ -0,0 +1,64 @@ +// 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 Sprache; + +namespace Perspex.Markup.Parsers.CSharp.Grammar +{ + internal class ExpressionGrammar + { + public static Parser ExpressionStatement() + { + return from expression in Expression().End() + select new ExpressionStatementSyntax(expression); + } + + public static Parser Expression() + { + return LiteralGrammar.Literal() + .Or(PrefixUnary()) + .Or(MemberAccess()) + .Or(ElementAccess()) + .Or(IdentifierGrammar.Identifier()); + } + + public static Parser MemberAccess() + { + return from identifier in IdentifierGrammar.Identifier() + from dot in Parse.Char('.') + from expression in Expression() + select new MemberAccessExpressionSyntax(expression, identifier); + } + + public static Parser ElementAccess() + { + return from expression in IdentifierGrammar.Identifier() + from arguments in BracketedArgumentList('[', ']') + select new ElementAccessExpressionSyntax(expression, arguments); + } + + public static Parser BracketedArgumentList( + char openBracket, + char closeBracket) + { + return from open in Parse.Char(openBracket) + from arguments in Arguments() + from close in Parse.Char(closeBracket) + select new BracketedArgumentListSyntax(arguments); + } + + public static Parser> Arguments() + { + return Expression().DelimitedBy(Parse.Char(',').Token()); + } + + public static Parser PrefixUnary() + { + return from bang in Parse.Char('!') + from operand in Expression() + select new PrefixUnaryExpressionSyntax(operand, SyntaxKind.LogicalNotExpression); + } + } +} diff --git a/src/Markup/Perspex.Markup/Parsers/CSharp/Grammar/IdentifierGrammar.cs b/src/Markup/Perspex.Markup/Parsers/CSharp/Grammar/IdentifierGrammar.cs new file mode 100644 index 0000000000..015a3f55c9 --- /dev/null +++ b/src/Markup/Perspex.Markup/Parsers/CSharp/Grammar/IdentifierGrammar.cs @@ -0,0 +1,41 @@ +using System.Globalization; +using System.Linq; +using Sprache; + +namespace Perspex.Markup.Parsers.CSharp.Grammar +{ + internal class IdentifierGrammar + { + private static readonly Parser CombiningCharacter = Parse.Char( + c => + { + var cat = CharUnicodeInfo.GetUnicodeCategory(c); + return cat == UnicodeCategory.NonSpacingMark || + cat == UnicodeCategory.SpacingCombiningMark; + }, + "Connecting Character"); + + private static readonly Parser ConnectingCharacter = Parse.Char( + c => CharUnicodeInfo.GetUnicodeCategory(c) == UnicodeCategory.ConnectorPunctuation, + "Connecting Character"); + + private static readonly Parser FormattingCharacter = Parse.Char( + c => CharUnicodeInfo.GetUnicodeCategory(c) == UnicodeCategory.Format, + "Connecting Character"); + + private static readonly Parser IdentifierStart = Parse.Letter.Or(Parse.Char('_')); + + private static readonly Parser IdentifierChar = Parse + .LetterOrDigit + .Or(ConnectingCharacter) + .Or(CombiningCharacter) + .Or(FormattingCharacter); + + public static Parser Identifier() + { + return from start in IdentifierStart.Once().Text() + from @char in IdentifierChar.Many().Text() + select new IdentifierSyntax(start + @char); + } + } +} diff --git a/src/Markup/Perspex.Markup/Parsers/CSharp/Grammar/LiteralGrammar.cs b/src/Markup/Perspex.Markup/Parsers/CSharp/Grammar/LiteralGrammar.cs new file mode 100644 index 0000000000..9cc4ea5705 --- /dev/null +++ b/src/Markup/Perspex.Markup/Parsers/CSharp/Grammar/LiteralGrammar.cs @@ -0,0 +1,20 @@ +using System.Globalization; +using System.Linq; +using Sprache; + +namespace Perspex.Markup.Parsers.CSharp.Grammar +{ + internal class LiteralGrammar + { + public static Parser Literal() + { + return Integer(); + } + + public static Parser Integer() + { + return from number in Parse.Number + select new LiteralExpressionSyntax(int.Parse(number), number); + } + } +} diff --git a/src/Markup/Perspex.Markup/Parsers/CSharp/IdentifierSyntax.cs b/src/Markup/Perspex.Markup/Parsers/CSharp/IdentifierSyntax.cs new file mode 100644 index 0000000000..dc676652d7 --- /dev/null +++ b/src/Markup/Perspex.Markup/Parsers/CSharp/IdentifierSyntax.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. + +using System; + +namespace Perspex.Markup.Parsers.CSharp +{ + internal class IdentifierSyntax : ExpressionSyntax + { + public IdentifierSyntax(string name) + { + Name = name; + } + + public String Name { get; } + } +} diff --git a/src/Markup/Perspex.Markup/Parsers/CSharp/LiteralExpressionSyntax.cs b/src/Markup/Perspex.Markup/Parsers/CSharp/LiteralExpressionSyntax.cs new file mode 100644 index 0000000000..bc741e8a77 --- /dev/null +++ b/src/Markup/Perspex.Markup/Parsers/CSharp/LiteralExpressionSyntax.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 Perspex.Markup.Parsers.CSharp +{ + internal class LiteralExpressionSyntax : ExpressionSyntax + { + public LiteralExpressionSyntax(object value, string valueText) + { + Value = value; + ValueText = valueText; + } + + public object Value { get; } + public string ValueText { get; } + } +} diff --git a/src/Markup/Perspex.Markup/Parsers/CSharp/MemberAccessExpressionSyntax.cs b/src/Markup/Perspex.Markup/Parsers/CSharp/MemberAccessExpressionSyntax.cs new file mode 100644 index 0000000000..3b019613af --- /dev/null +++ b/src/Markup/Perspex.Markup/Parsers/CSharp/MemberAccessExpressionSyntax.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 Perspex.Markup.Parsers.CSharp +{ + internal class MemberAccessExpressionSyntax : ExpressionSyntax + { + public MemberAccessExpressionSyntax(ExpressionSyntax expression, IdentifierSyntax member) + { + Expression = expression; + Member = member; + } + + public ExpressionSyntax Expression { get; } + public IdentifierSyntax Member { get; } + } +} diff --git a/src/Markup/Perspex.Markup/Parsers/CSharp/PrefixUnaryExpressionSyntax.cs b/src/Markup/Perspex.Markup/Parsers/CSharp/PrefixUnaryExpressionSyntax.cs new file mode 100644 index 0000000000..f0e292ba33 --- /dev/null +++ b/src/Markup/Perspex.Markup/Parsers/CSharp/PrefixUnaryExpressionSyntax.cs @@ -0,0 +1,23 @@ +// 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.Parsers.CSharp +{ + internal class PrefixUnaryExpressionSyntax : ExpressionSyntax + { + private SyntaxKind _kind; + + public PrefixUnaryExpressionSyntax(ExpressionSyntax operand, SyntaxKind kind) + { + Operand = operand; + _kind = kind; + } + + public ExpressionSyntax Operand { get; } + + public SyntaxKind Kind() + { + return _kind; + } + } +} diff --git a/src/Markup/Perspex.Markup/Parsers/CSharp/SyntaxKind.cs b/src/Markup/Perspex.Markup/Parsers/CSharp/SyntaxKind.cs new file mode 100644 index 0000000000..121bc48b9f --- /dev/null +++ b/src/Markup/Perspex.Markup/Parsers/CSharp/SyntaxKind.cs @@ -0,0 +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. + +namespace Perspex.Markup.Parsers.CSharp +{ + internal enum SyntaxKind + { + LogicalNotExpression, + } +} diff --git a/src/Markup/Perspex.Markup/Parsers/CSharp/SyntaxNode.cs b/src/Markup/Perspex.Markup/Parsers/CSharp/SyntaxNode.cs new file mode 100644 index 0000000000..ea9e71d870 --- /dev/null +++ b/src/Markup/Perspex.Markup/Parsers/CSharp/SyntaxNode.cs @@ -0,0 +1,9 @@ +// 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.Parsers.CSharp +{ + internal class SyntaxNode + { + } +} diff --git a/src/Markup/Perspex.Markup/Parsers/CSharp/SyntaxToken.cs b/src/Markup/Perspex.Markup/Parsers/CSharp/SyntaxToken.cs new file mode 100644 index 0000000000..e66aa4bde0 --- /dev/null +++ b/src/Markup/Perspex.Markup/Parsers/CSharp/SyntaxToken.cs @@ -0,0 +1,15 @@ +// 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.Parsers.CSharp +{ + internal class SyntaxToken + { + public SyntaxToken(string valueText) + { + ValueText = valueText; + } + + public string ValueText { get; } + } +} diff --git a/src/Markup/Perspex.Markup/Perspex.Markup.csproj b/src/Markup/Perspex.Markup/Perspex.Markup.csproj index fd08d6e577..9b9d696c95 100644 --- a/src/Markup/Perspex.Markup/Perspex.Markup.csproj +++ b/src/Markup/Perspex.Markup/Perspex.Markup.csproj @@ -37,22 +37,28 @@ + + + + + + + + + + + + + + - - ..\..\..\packages\Microsoft.CodeAnalysis.Common.1.1.0-beta1-20150812-01\lib\portable-net45+win8\Microsoft.CodeAnalysis.dll - True - - - ..\..\..\packages\Microsoft.CodeAnalysis.CSharp.1.1.0-beta1-20150812-01\lib\portable-net45+win8\Microsoft.CodeAnalysis.CSharp.dll - True - - - ..\..\..\packages\System.Collections.Immutable.1.1.36\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll + + ..\..\..\packages\Sprache.2.0.0.47\lib\portable-net4+netcore45+win8+wp8+sl5+MonoAndroid1+MonoTouch1\Sprache.dll True @@ -71,18 +77,10 @@ ..\..\..\packages\Rx-PlatformServices.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.PlatformServices.dll True - - ..\..\..\packages\System.Reflection.Metadata.1.1.0-alpha-00009\lib\portable-net45+win8\System.Reflection.Metadata.dll - True - - - - - From 3cb479769df5d4736eb6354e13840ea69376aa7d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 2 Oct 2015 02:04:36 +0200 Subject: [PATCH 26/72] Use string XamlBindingDefinition.SourcePropertyPath. --- .../DataBinding/PerspexPropertyBinder.cs | 3 +- .../DataBinding/XamlBindingDefinition.cs | 28 +++++++++---------- .../MarkupExtensions/BindingExtension.cs | 8 +----- .../BindingDefinitionBuilder.cs | 6 ++-- 4 files changed, 18 insertions(+), 27 deletions(-) diff --git a/src/Markup/Perspex.Markup.Xaml/DataBinding/PerspexPropertyBinder.cs b/src/Markup/Perspex.Markup.Xaml/DataBinding/PerspexPropertyBinder.cs index 27f93d53e9..9b53ef52c7 100644 --- a/src/Markup/Perspex.Markup.Xaml/DataBinding/PerspexPropertyBinder.cs +++ b/src/Markup/Perspex.Markup.Xaml/DataBinding/PerspexPropertyBinder.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using OmniXaml.TypeConversion; +using Perspex.Markup.Xaml.DataBinding.ChangeTracking; namespace Perspex.Markup.Xaml.DataBinding { @@ -47,7 +48,7 @@ namespace Perspex.Markup.Xaml.DataBinding var binding = new XamlBinding(_typeConverterProvider) { BindingMode = xamlBinding.BindingMode, - SourcePropertyPath = xamlBinding.SourcePropertyPath, + SourcePropertyPath = new PropertyPath(xamlBinding.SourcePropertyPath), Target = xamlBinding.Target, TargetProperty = xamlBinding.TargetProperty }; diff --git a/src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBindingDefinition.cs b/src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBindingDefinition.cs index cce86455d6..62ebcf39ca 100644 --- a/src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBindingDefinition.cs +++ b/src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBindingDefinition.cs @@ -2,31 +2,29 @@ // 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) + public XamlBindingDefinition( + Control target, + PerspexProperty targetProperty, + string sourcePropertyPath, + BindingMode bindingMode) { - _target = target; - _targetProperty = targetProperty; - _sourcePropertyPath = sourcePropertyPath; - _bindingMode = bindingMode; + Target = target; + TargetProperty = targetProperty; + SourcePropertyPath = sourcePropertyPath; + BindingMode = bindingMode; } - public Control Target => _target; + public Control Target { get; } - public PerspexProperty TargetProperty => _targetProperty; + public PerspexProperty TargetProperty { get; } - public PropertyPath SourcePropertyPath => _sourcePropertyPath; + public string SourcePropertyPath { get; } - public BindingMode BindingMode => _bindingMode; + public BindingMode BindingMode { get; } } } \ 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..593d245dba 100644 --- a/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs @@ -27,13 +27,7 @@ namespace Perspex.Markup.Xaml.MarkupExtensions 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(target, perspexProperty, Path, Mode); } /// The source path (for CLR bindings). diff --git a/tests/Perspex.Markup.Xaml.UnitTests/BindingDefinitionBuilder.cs b/tests/Perspex.Markup.Xaml.UnitTests/BindingDefinitionBuilder.cs index e968db3615..066a1ea12b 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/BindingDefinitionBuilder.cs +++ b/tests/Perspex.Markup.Xaml.UnitTests/BindingDefinitionBuilder.cs @@ -1,24 +1,22 @@ // 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 readonly string _sourcePropertyPath; private Control _target; private PerspexProperty _targetProperty; public BindingDefinitionBuilder() { _bindingMode = BindingMode.Default; - _sourcePropertyPath = new PropertyPath(string.Empty); + _sourcePropertyPath = string.Empty; } public BindingDefinitionBuilder WithNullTarget() From f0f22f06ad2b9df3f244f5f08ae8211c07f0752a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 2 Oct 2015 02:16:15 +0200 Subject: [PATCH 27/72] Select correct binding mode. --- src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBinding.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBinding.cs b/src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBinding.cs index af0d33de6a..837413a26f 100644 --- a/src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBinding.cs +++ b/src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBinding.cs @@ -37,21 +37,22 @@ namespace Perspex.Markup.Xaml.DataBinding { var bindingSource = new DataContextChangeSynchronizer.BindingSource(SourcePropertyPath, dataContext); var bindingTarget = new DataContextChangeSynchronizer.BindingTarget(Target, TargetProperty); + var mode = BindingMode == BindingMode.Default ? TargetProperty.DefaultBindingMode : BindingMode; _changeSynchronizer = new DataContextChangeSynchronizer(bindingSource, bindingTarget, _typeConverterProvider); - if (BindingMode == BindingMode.TwoWay) + if (mode == BindingMode.TwoWay) { _changeSynchronizer.StartUpdatingTargetWhenSourceChanges(); _changeSynchronizer.StartUpdatingSourceWhenTargetChanges(); } - if (BindingMode == BindingMode.OneWay) + if (mode == BindingMode.OneWay || mode == BindingMode.Default) { _changeSynchronizer.StartUpdatingTargetWhenSourceChanges(); } - if (BindingMode == BindingMode.OneWayToSource) + if (mode == BindingMode.OneWayToSource) { _changeSynchronizer.StartUpdatingSourceWhenTargetChanges(); } From d55e7fa9d0a80b3e27a6e030e3f64bb14f07abce Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 2 Oct 2015 02:56:52 +0200 Subject: [PATCH 28/72] Search base classes for properties. --- .../Binding/PropertyAccessorNode.cs | 23 ++++++++++++++++++- .../ExpressionObserverTests_Property.cs | 15 ++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs b/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs index 08ecafac6a..c1a45d8784 100644 --- a/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs +++ b/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs @@ -46,7 +46,7 @@ namespace Perspex.Markup.Binding if (target != null) { - _propertyInfo = target.GetType().GetTypeInfo().GetDeclaredProperty(PropertyName); + _propertyInfo = FindProperty(target, PropertyName); if (_propertyInfo != null) { @@ -82,6 +82,27 @@ namespace Perspex.Markup.Binding } } + 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); diff --git a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Property.cs b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Property.cs index ccda547bd5..ac159a2deb 100644 --- a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Property.cs +++ b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Property.cs @@ -22,6 +22,17 @@ namespace Perspex.Markup.UnitTests.Binding 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() { @@ -235,6 +246,10 @@ namespace Perspex.Markup.UnitTests.Binding } } + private class Class3 : Class1 + { + } + private class WithoutBar : NotifyingBase, INext { } From 52975c84328ed86e552054ce746772d407dbbf8f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 2 Oct 2015 10:29:36 +0200 Subject: [PATCH 29/72] Basic binding using ExpressionObserver. --- samples/XamlTestApplication/MainViewModel.cs | 52 ------ .../ViewModels/MainWindowViewModel.cs | 58 ++++++ .../ViewModels/TestItem.cs | 17 ++ .../ViewModels/TestNode.cs | 14 ++ .../XamlTestApplication/Views/MainWindow.cs | 9 +- .../XamlTestApplication/Views/MainWindow.paml | 16 +- .../XamlTestApplication.csproj | 4 +- .../Context/PerspexXamlMemberValuePlugin.cs | 22 ++- .../ObservablePropertyBranch.cs | 109 ------------ .../ChangeTracking/PropertyMountPoint.cs | 52 ------ .../ChangeTracking/PropertyPath.cs | 36 ---- .../ChangeTracking/TargettedProperty.cs | 41 ----- .../DataContextChangeSynchronizer.cs | 165 ------------------ .../DataBinding/IPerspexPropertyBinder.cs | 3 + .../DataBinding/PerspexPropertyBinder.cs | 8 +- .../DataBinding/XamlBinding.cs | 57 +++--- .../DataBinding/XamlBindingDefinition.cs | 31 ++-- .../MarkupExtensions/BindingExtension.cs | 12 +- .../Perspex.Markup.Xaml.csproj | 9 +- .../Properties/AssemblyInfo.cs | 1 + .../BindingDefinitionBuilder.cs | 8 +- .../ChangeBranchTest.cs | 72 -------- .../DataContextChangeSynchronizerTest.cs | 103 ----------- .../Perspex.Markup.Xaml.UnitTests.csproj | 3 - .../PropertyMountPointTest.cs | 18 -- .../XamlBindingTest.cs | 6 +- 26 files changed, 169 insertions(+), 757 deletions(-) delete mode 100644 samples/XamlTestApplication/MainViewModel.cs create mode 100644 samples/XamlTestApplication/ViewModels/MainWindowViewModel.cs create mode 100644 samples/XamlTestApplication/ViewModels/TestItem.cs create mode 100644 samples/XamlTestApplication/ViewModels/TestNode.cs delete mode 100644 src/Markup/Perspex.Markup.Xaml/DataBinding/ChangeTracking/ObservablePropertyBranch.cs delete mode 100644 src/Markup/Perspex.Markup.Xaml/DataBinding/ChangeTracking/PropertyMountPoint.cs delete mode 100644 src/Markup/Perspex.Markup.Xaml/DataBinding/ChangeTracking/PropertyPath.cs delete mode 100644 src/Markup/Perspex.Markup.Xaml/DataBinding/ChangeTracking/TargettedProperty.cs delete mode 100644 src/Markup/Perspex.Markup.Xaml/DataBinding/DataContextChangeSynchronizer.cs delete mode 100644 tests/Perspex.Markup.Xaml.UnitTests/ChangeBranchTest.cs delete mode 100644 tests/Perspex.Markup.Xaml.UnitTests/DataContextChangeSynchronizerTest.cs delete mode 100644 tests/Perspex.Markup.Xaml.UnitTests/PropertyMountPointTest.cs diff --git a/samples/XamlTestApplication/MainViewModel.cs b/samples/XamlTestApplication/MainViewModel.cs deleted file mode 100644 index 450c8f422f..0000000000 --- a/samples/XamlTestApplication/MainViewModel.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.Collections.Generic; -using ReactiveUI; - -namespace XamlTestApplication -{ - public class MainViewModel : ReactiveObject - { - private string _name; - - public MainViewModel() - { - Name = "Jos\u00E9 Manuel"; - People = new List - { - new Person("a little bit of Monica in my life"), - new Person("a little bit of Erica by my side"), - new Person("a little bit of Rita is all I need"), - new Person("a little bit of Tina is what I see"), - new Person("a little bit of Sandra in the sun"), - new Person("a little bit of Mary all night long"), - new Person("a little bit of Jessica here I am"), - }; - } - - public string Name - { - get { return _name; } - set { this.RaiseAndSetIfChanged(ref _name, value); } - } - - public List People { get; set; } - } - - public class Person - { - private string _name; - - public Person(string name) - { - _name = name; - } - - public string Name - { - get { return _name; } - set { _name = value; } - } - } -} \ No newline at end of file diff --git a/samples/XamlTestApplication/ViewModels/MainWindowViewModel.cs b/samples/XamlTestApplication/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000000..b786ee8f9b --- /dev/null +++ b/samples/XamlTestApplication/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; } + } +} \ No newline at end of file diff --git a/samples/XamlTestApplication/ViewModels/TestItem.cs b/samples/XamlTestApplication/ViewModels/TestItem.cs new file mode 100644 index 0000000000..30ed5d03ee --- /dev/null +++ b/samples/XamlTestApplication/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; } + } +} \ No newline at end of file diff --git a/samples/XamlTestApplication/ViewModels/TestNode.cs b/samples/XamlTestApplication/ViewModels/TestNode.cs new file mode 100644 index 0000000000..953bfd0f58 --- /dev/null +++ b/samples/XamlTestApplication/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/XamlTestApplication/Views/MainWindow.cs b/samples/XamlTestApplication/Views/MainWindow.cs index dc218641bf..d18985a898 100644 --- a/samples/XamlTestApplication/Views/MainWindow.cs +++ b/samples/XamlTestApplication/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/XamlTestApplication/Views/MainWindow.paml b/samples/XamlTestApplication/Views/MainWindow.paml index 0794d15aff..d6ab54e858 100644 --- a/samples/XamlTestApplication/Views/MainWindow.paml +++ b/samples/XamlTestApplication/Views/MainWindow.paml @@ -48,20 +48,8 @@ - - - - - - - - - - - - - - + + diff --git a/samples/XamlTestApplication/XamlTestApplication.csproj b/samples/XamlTestApplication/XamlTestApplication.csproj index d70e2e3297..107df97bcc 100644 --- a/samples/XamlTestApplication/XamlTestApplication.csproj +++ b/samples/XamlTestApplication/XamlTestApplication.csproj @@ -77,9 +77,11 @@ - + + + diff --git a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs index 423ca66e23..f978ecaa69 100644 --- a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs +++ b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs @@ -54,22 +54,26 @@ namespace Perspex.Markup.Xaml.Context po.SetValue(pp, value); } - private void HandleXamlBindingDefinition(XamlBindingDefinition xamlBindingDefinition) + private void HandleXamlBindingDefinition(XamlBindingDefinition def) { - PerspexObject subjectObject = xamlBindingDefinition.Target; - _propertyBinder.Create(xamlBindingDefinition); + var binding = new XamlBinding(_propertyBinder.TypeConverterProvider) + { + BindingMode = def.BindingMode, + SourcePropertyPath = def.SourcePropertyPath, + Target = def.Target, + TargetProperty = def.TargetProperty, + }; - var observableForDataContext = subjectObject.GetObservable(Control.DataContextProperty); - observableForDataContext.Where(o => o != null).Subscribe(_ => BindToDataContextWhenItsSet(xamlBindingDefinition)); + binding.Bind(); } private void BindToDataContextWhenItsSet(XamlBindingDefinition definition) { - var target = definition.Target; - var dataContext = target.DataContext; + // var target = definition.Target; + // var dataContext = target.DataContext; - var binding = _propertyBinder.GetBinding(target, definition.TargetProperty); - binding.BindToDataContext(dataContext); + // var binding = _propertyBinder.GetBinding(target, definition.TargetProperty); + // binding.BindToDataContext(dataContext); } // ReSharper disable once MemberCanBePrivate.Global 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 index d7879dc29b..40d349e00d 100644 --- a/src/Markup/Perspex.Markup.Xaml/DataBinding/IPerspexPropertyBinder.cs +++ b/src/Markup/Perspex.Markup.Xaml/DataBinding/IPerspexPropertyBinder.cs @@ -2,11 +2,14 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System.Collections.Generic; +using OmniXaml.TypeConversion; namespace Perspex.Markup.Xaml.DataBinding { public interface IPerspexPropertyBinder { + ITypeConverterProvider TypeConverterProvider { get; } + XamlBinding GetBinding(PerspexObject po, PerspexProperty pp); IEnumerable GetBindings(PerspexObject source); diff --git a/src/Markup/Perspex.Markup.Xaml/DataBinding/PerspexPropertyBinder.cs b/src/Markup/Perspex.Markup.Xaml/DataBinding/PerspexPropertyBinder.cs index 27f93d53e9..916e3edd85 100644 --- a/src/Markup/Perspex.Markup.Xaml/DataBinding/PerspexPropertyBinder.cs +++ b/src/Markup/Perspex.Markup.Xaml/DataBinding/PerspexPropertyBinder.cs @@ -10,16 +10,16 @@ namespace Perspex.Markup.Xaml.DataBinding { public class PerspexPropertyBinder : IPerspexPropertyBinder { - private readonly ITypeConverterProvider _typeConverterProvider; - private readonly HashSet _bindings; public PerspexPropertyBinder(ITypeConverterProvider typeConverterProvider) { - _typeConverterProvider = typeConverterProvider; + TypeConverterProvider = typeConverterProvider; _bindings = new HashSet(); } + public ITypeConverterProvider TypeConverterProvider { get; } + public XamlBinding GetBinding(PerspexObject po, PerspexProperty pp) { return _bindings.First(xamlBinding => xamlBinding.Target == po && xamlBinding.TargetProperty == pp); @@ -44,7 +44,7 @@ namespace Perspex.Markup.Xaml.DataBinding throw new InvalidOperationException(); } - var binding = new XamlBinding(_typeConverterProvider) + var binding = new XamlBinding(TypeConverterProvider) { BindingMode = xamlBinding.BindingMode, SourcePropertyPath = xamlBinding.SourcePropertyPath, diff --git a/src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBinding.cs b/src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBinding.cs index af0d33de6a..c23a5bce22 100644 --- a/src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBinding.cs +++ b/src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBinding.cs @@ -2,16 +2,15 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Diagnostics; +using System.Reactive.Linq; using OmniXaml.TypeConversion; -using Perspex.Markup.Xaml.DataBinding.ChangeTracking; +using Perspex.Markup.Binding; namespace Perspex.Markup.Xaml.DataBinding { public class XamlBinding { private readonly ITypeConverterProvider _typeConverterProvider; - private DataContextChangeSynchronizer _changeSynchronizer; public XamlBinding(ITypeConverterProvider typeConverterProvider) { @@ -22,43 +21,43 @@ namespace Perspex.Markup.Xaml.DataBinding public PerspexProperty TargetProperty { get; set; } - public PropertyPath SourcePropertyPath { get; set; } + public object Source { get; set; } + + public string SourcePropertyPath { get; set; } public BindingMode BindingMode { get; set; } - public void BindToDataContext(object dataContext) + public void Bind() { - if (dataContext == null) - { - return; - } + var path = SourcePropertyPath; + var source = Source; - try + if (source == null) { - 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) + if (!string.IsNullOrWhiteSpace(path)) { - _changeSynchronizer.StartUpdatingTargetWhenSourceChanges(); + path = "DataContext." + path; } - if (BindingMode == BindingMode.OneWayToSource) - { - _changeSynchronizer.StartUpdatingSourceWhenTargetChanges(); - } + source = Target; } - catch (Exception e) + + var observable = new ExpressionObserver(source, path); + var mode = BindingMode == BindingMode.Default ? + TargetProperty.DefaultBindingMode : BindingMode; + + switch (mode) { - Debug.WriteLine(e); + case BindingMode.Default: + case BindingMode.OneWay: + Target.Bind(TargetProperty, observable.Select(x => x.Value)); + break; + case BindingMode.TwoWay: + throw new NotImplementedException(); + case BindingMode.OneTime: + throw new NotImplementedException(); + case BindingMode.OneWayToSource: + throw new NotImplementedException(); } } } diff --git a/src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBindingDefinition.cs b/src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBindingDefinition.cs index cce86455d6..4cbd3bfe1e 100644 --- a/src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBindingDefinition.cs +++ b/src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBindingDefinition.cs @@ -2,31 +2,26 @@ // 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) + public XamlBindingDefinition( + Control target, + PerspexProperty targetProperty, + string sourcePropertyPath, + BindingMode bindingMode) { - _target = target; - _targetProperty = targetProperty; - _sourcePropertyPath = 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; + public Control Target { get; } + public PerspexProperty TargetProperty { get; } + public string SourcePropertyPath { get; } + public BindingMode BindingMode { get; } } } \ 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..55f2b1ee4a 100644 --- a/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs @@ -5,7 +5,6 @@ using System.Linq; using OmniXaml; using Perspex.Controls; using Perspex.Markup.Xaml.DataBinding; -using Perspex.Markup.Xaml.DataBinding.ChangeTracking; namespace Perspex.Markup.Xaml.MarkupExtensions { @@ -26,19 +25,10 @@ namespace Perspex.Markup.Xaml.MarkupExtensions 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(target, perspexProperty, 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/Perspex.Markup.Xaml.csproj b/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj index f6b9dcf204..d3de32e339 100644 --- a/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj +++ b/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj @@ -54,10 +54,6 @@ - - - - @@ -228,7 +224,6 @@ - @@ -289,6 +284,10 @@ {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F} Perspex.Styling + + {6417e941-21bc-467b-a771-0de389353ce6} + Perspex.Markup + 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/tests/Perspex.Markup.Xaml.UnitTests/BindingDefinitionBuilder.cs b/tests/Perspex.Markup.Xaml.UnitTests/BindingDefinitionBuilder.cs index e968db3615..5c85e79202 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/BindingDefinitionBuilder.cs +++ b/tests/Perspex.Markup.Xaml.UnitTests/BindingDefinitionBuilder.cs @@ -4,21 +4,19 @@ 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 readonly string _sourcePropertyPath; private Control _target; - private PerspexProperty _targetProperty; public BindingDefinitionBuilder() { _bindingMode = BindingMode.Default; - _sourcePropertyPath = new PropertyPath(string.Empty); + _sourcePropertyPath = string.Empty; } public BindingDefinitionBuilder WithNullTarget() @@ -33,7 +31,7 @@ namespace Perspex.Xaml.Base.UnitTest bindingMode: _bindingMode, sourcePropertyPath: _sourcePropertyPath, target: _target, - targetProperty: _targetProperty); + targetProperty: null); } } } \ 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..b74704c7fa 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj +++ b/tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj @@ -88,9 +88,7 @@ - - @@ -105,7 +103,6 @@ - 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..fcd90f6333 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/XamlBindingTest.cs +++ b/tests/Perspex.Markup.Xaml.UnitTests/XamlBindingTest.cs @@ -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); } } } From 82be7441ad1d99385caea37fdbc6f804c74f33a7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 2 Oct 2015 10:50:36 +0200 Subject: [PATCH 30/72] Added ExpressionSubject. --- .../Binding/ExpressionObserver.cs | 4 +- .../Binding/ExpressionSubject.cs | 48 +++++++++++++++++++ .../Perspex.Markup/Perspex.Markup.csproj | 1 + 3 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 src/Markup/Perspex.Markup/Binding/ExpressionSubject.cs diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs b/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs index bd0b08b7c3..c50868b549 100644 --- a/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs +++ b/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs @@ -2,15 +2,13 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections.Generic; -using System.Linq; using System.Reactive; using System.Reactive.Disposables; namespace Perspex.Markup.Binding { /// - /// Observes the value of an expression on a root object. + /// Observes and sets the value of an expression on an object. /// public class ExpressionObserver : ObservableBase { diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionSubject.cs b/src/Markup/Perspex.Markup/Binding/ExpressionSubject.cs new file mode 100644 index 0000000000..e2e94daf7c --- /dev/null +++ b/src/Markup/Perspex.Markup/Binding/ExpressionSubject.cs @@ -0,0 +1,48 @@ +// 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 + { + private ExpressionObserver _inner; + + /// + /// Initializes a new instance of the class. + /// + /// The . + public ExpressionSubject(ExpressionObserver inner) + { + _inner = inner; + } + + /// + 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).Distinct().Subscribe(observer); + } + } +} diff --git a/src/Markup/Perspex.Markup/Perspex.Markup.csproj b/src/Markup/Perspex.Markup/Perspex.Markup.csproj index aee9048774..d22201ee9f 100644 --- a/src/Markup/Perspex.Markup/Perspex.Markup.csproj +++ b/src/Markup/Perspex.Markup/Perspex.Markup.csproj @@ -36,6 +36,7 @@ + From 4d5abf80e7dbd64833baa16b100afa115de00a4c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 2 Oct 2015 11:43:08 +0200 Subject: [PATCH 31/72] Added a binding test application. --- Perspex.sln | 7 + samples/BindingTest/App.config | 6 + samples/BindingTest/App.cs | 31 ++++ samples/BindingTest/BindingTest.csproj | 142 ++++++++++++++++++ samples/BindingTest/MainWindow.paml | 6 + samples/BindingTest/MainWindow.paml.cs | 21 +++ .../BindingTest/Properties/AssemblyInfo.cs | 36 +++++ .../ViewModels/MainWindowViewModel.cs | 15 ++ samples/BindingTest/packages.config | 4 + 9 files changed, 268 insertions(+) create mode 100644 samples/BindingTest/App.config create mode 100644 samples/BindingTest/App.cs create mode 100644 samples/BindingTest/BindingTest.csproj create mode 100644 samples/BindingTest/MainWindow.paml create mode 100644 samples/BindingTest/MainWindow.paml.cs create mode 100644 samples/BindingTest/Properties/AssemblyInfo.cs create mode 100644 samples/BindingTest/ViewModels/MainWindowViewModel.cs create mode 100644 samples/BindingTest/packages.config diff --git a/Perspex.sln b/Perspex.sln index 3b97ab21cf..1dc7c56517 100644 --- a/Perspex.sln +++ b/Perspex.sln @@ -100,6 +100,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Markup", "src\Marku 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 Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\PlatformSupport\PlatformSupport.projitems*{e4d9629c-f168-4224-3f51-a5e482ffbc42}*SharedItemsImports = 13 @@ -247,6 +249,10 @@ Global {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -274,5 +280,6 @@ Global {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} 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..df2ba5a586 --- /dev/null +++ b/samples/BindingTest/App.cs @@ -0,0 +1,31 @@ +using System; +using Perspex; +using Perspex.Controls; +using Perspex.Diagnostics; +using Perspex.Themes.Default; + +namespace BindingTest +{ + public class App : Application + { + public App() + { + RegisterServices(); + InitializeSubsystems((int)Environment.OSVersion.Platform); + Styles = new DefaultTheme(); + } + + 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..0e0925adb2 --- /dev/null +++ b/samples/BindingTest/BindingTest.csproj @@ -0,0 +1,142 @@ + + + + + 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\Splat.1.6.2\lib\Net45\Splat.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..2c7b0681d1 --- /dev/null +++ b/samples/BindingTest/MainWindow.paml @@ -0,0 +1,6 @@ + + + + + + \ 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..e8ba113eb8 --- /dev/null +++ b/samples/BindingTest/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,15 @@ +using ReactiveUI; + +namespace BindingTest.ViewModels +{ + public class MainWindowViewModel : ReactiveObject + { + private string _simpleBinding = "Simple Binding"; + + public string SimpleBinding + { + get { return _simpleBinding; } + set { this.RaiseAndSetIfChanged(ref _simpleBinding, value); } + } + } +} diff --git a/samples/BindingTest/packages.config b/samples/BindingTest/packages.config new file mode 100644 index 0000000000..85a1a3cdc5 --- /dev/null +++ b/samples/BindingTest/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file From 907505f1941842ff3e92da56df745f0ebb475c85 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 2 Oct 2015 12:12:40 +0200 Subject: [PATCH 32/72] Get simple two-way bindings working. --- samples/BindingTest/App.cs | 9 +++ samples/BindingTest/BindingTest.csproj | 8 +++ samples/BindingTest/packages.config | 1 + .../DataBinding/XamlBinding.cs | 3 +- .../Binding/ExpressionSubject.cs | 2 +- src/Perspex.Base/PerspexObject.cs | 62 ++++++++++++++++--- src/Perspex.Controls/TextBlock.cs | 2 +- .../Perspex.Markup.Xaml.UnitTests.csproj | 7 ++- .../packages.config | 2 +- 9 files changed, 82 insertions(+), 14 deletions(-) diff --git a/samples/BindingTest/App.cs b/samples/BindingTest/App.cs index df2ba5a586..79222817cd 100644 --- a/samples/BindingTest/App.cs +++ b/samples/BindingTest/App.cs @@ -3,6 +3,8 @@ using Perspex; using Perspex.Controls; using Perspex.Diagnostics; using Perspex.Themes.Default; +using Serilog; +using Serilog.Filters; namespace BindingTest { @@ -13,6 +15,13 @@ namespace BindingTest RegisterServices(); InitializeSubsystems((int)Environment.OSVersion.Platform); Styles = new DefaultTheme(); + + Log.Logger = new LoggerConfiguration() + .Filter.ByIncludingOnly(Matching.WithProperty("Area", "Property")) + .Filter.ByIncludingOnly(Matching.WithProperty("Property", x => x == "Text")) + .MinimumLevel.Verbose() + .WriteTo.Trace(outputTemplate: "[{Id:X8}] [{SourceContext}] {Message}") + .CreateLogger(); } public static void AttachDevTools(Window window) diff --git a/samples/BindingTest/BindingTest.csproj b/samples/BindingTest/BindingTest.csproj index 0e0925adb2..f98cc1dd08 100644 --- a/samples/BindingTest/BindingTest.csproj +++ b/samples/BindingTest/BindingTest.csproj @@ -36,6 +36,14 @@ + + ..\..\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 diff --git a/samples/BindingTest/packages.config b/samples/BindingTest/packages.config index 85a1a3cdc5..6a1eddec98 100644 --- a/samples/BindingTest/packages.config +++ b/samples/BindingTest/packages.config @@ -1,4 +1,5 @@  + \ 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 index c23a5bce22..8bf09ec63c 100644 --- a/src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBinding.cs +++ b/src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBinding.cs @@ -53,7 +53,8 @@ namespace Perspex.Markup.Xaml.DataBinding Target.Bind(TargetProperty, observable.Select(x => x.Value)); break; case BindingMode.TwoWay: - throw new NotImplementedException(); + Target.BindTwoWay(TargetProperty, new ExpressionSubject(observable)); + break; case BindingMode.OneTime: throw new NotImplementedException(); case BindingMode.OneWayToSource: diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionSubject.cs b/src/Markup/Perspex.Markup/Binding/ExpressionSubject.cs index e2e94daf7c..cd076c9ab7 100644 --- a/src/Markup/Perspex.Markup/Binding/ExpressionSubject.cs +++ b/src/Markup/Perspex.Markup/Binding/ExpressionSubject.cs @@ -42,7 +42,7 @@ namespace Perspex.Markup.Binding /// public IDisposable Subscribe(IObserver observer) { - return _inner.Select(x => x.Value).Distinct().Subscribe(observer); + return _inner.Select(x => x.Value).Subscribe(observer); } } } diff --git a/src/Perspex.Base/PerspexObject.cs b/src/Perspex.Base/PerspexObject.cs index 7a0e211f2c..0c27fe9453 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; @@ -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 @@ -658,7 +656,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 +674,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, + source, + priority); + + return new CompositeDisposable( + Bind(property, source), + GetObservable(property).Subscribe(source)); + } + /// /// Forces the specified property to be revalidated. /// @@ -931,6 +964,21 @@ namespace Perspex return string.Format("{0}.{1}", GetType().Name, property.Name); } + /// + /// 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.Controls/TextBlock.cs b/src/Perspex.Controls/TextBlock.cs index 607754dcb2..904e2d81d7 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/tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj b/tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj index b74704c7fa..6c851a1e87 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 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 @@ - + From 8457645eb456d0796b703068079a4fe6c15d5fd7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 2 Oct 2015 12:32:41 +0200 Subject: [PATCH 33/72] Implemented OneWayToSource binding. --- samples/BindingTest/MainWindow.paml | 7 +++++-- src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBinding.cs | 3 ++- src/Perspex.Base/BindingDescriptor.cs | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/samples/BindingTest/MainWindow.paml b/samples/BindingTest/MainWindow.paml index 2c7b0681d1..38e7c14d40 100644 --- a/samples/BindingTest/MainWindow.paml +++ b/samples/BindingTest/MainWindow.paml @@ -1,6 +1,9 @@  - + - + + + + \ 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 index 8bf09ec63c..330a835fc6 100644 --- a/src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBinding.cs +++ b/src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBinding.cs @@ -58,7 +58,8 @@ namespace Perspex.Markup.Xaml.DataBinding case BindingMode.OneTime: throw new NotImplementedException(); case BindingMode.OneWayToSource: - throw new NotImplementedException(); + Target.GetObservable(TargetProperty).Subscribe(new ExpressionSubject(observable)); + break; } } } 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, From 072aa3c96de3fcf4b9c94fba398123801870ac50 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 2 Oct 2015 12:48:00 +0200 Subject: [PATCH 34/72] Allow ExpressionObserver root to be changed. --- .../Binding/ExpressionObserver.cs | 26 +++++++++++++++--- .../ExpressionObserverTests_Property.cs | 27 +++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs b/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs index c50868b549..fc7a4b2663 100644 --- a/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs +++ b/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs @@ -12,6 +12,7 @@ namespace Perspex.Markup.Binding /// public class ExpressionObserver : ObservableBase { + private object _root; private int _count; private ExpressionNode _node; @@ -22,7 +23,7 @@ namespace Perspex.Markup.Binding /// The expression. public ExpressionObserver(object root, string expression) { - Root = root; + _root = root; _node = ExpressionNodeBuilder.Build(expression); } @@ -49,9 +50,28 @@ namespace Perspex.Markup.Binding } /// - /// Gets the root object that the expression is being observed on. + /// Gets or sets the root object that the expression is being observed on. /// - public object Root { get; } + public object Root + { + get + { + return _root; + } + + set + { + if (_root != value) + { + _root = value; + + if (_count > 0) + { + _node.Target = _root; + } + } + } + } /// protected override IDisposable SubscribeCore(IObserver observer) diff --git a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Property.cs b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Property.cs index ac159a2deb..3518cfa52d 100644 --- a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Property.cs +++ b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Property.cs @@ -200,6 +200,33 @@ namespace Perspex.Markup.UnitTests.Binding 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; } From d100f49ba8e261c51a7aeaf10630c2d5138093cd Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 2 Oct 2015 12:52:17 +0200 Subject: [PATCH 35/72] Renamed namespace DataBinding -> Binding. --- .../IPerspexPropertyBinder.cs | 2 +- .../PerspexPropertyBinder.cs | 2 +- .../SourceBindingEndpoint.cs | 2 +- .../TargetBindingEndpoint.cs | 2 +- .../{DataBinding => Binding}/XamlBinding.cs | 2 +- .../XamlBindingDefinition.cs | 2 +- .../Context/PerspexTypeRepository.cs | 2 +- .../Context/PerspexWiringContext.cs | 2 +- .../Perspex.Markup.Xaml/Context/PerspexXamlMember.cs | 2 +- .../Context/PerspexXamlMemberValuePlugin.cs | 2 +- .../Perspex.Markup.Xaml/Context/PerspexXamlType.cs | 2 +- .../MarkupExtensions/BindingExtension.cs | 2 +- .../Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj | 12 ++++++------ .../Templates/TreeDataTemplate.cs | 2 +- tests/Perspex.Markup.Xaml.UnitTests/BinderTest.cs | 2 +- .../BindingDefinitionBuilder.cs | 2 +- .../Perspex.Markup.Xaml.UnitTests/XamlBindingTest.cs | 2 +- 17 files changed, 22 insertions(+), 22 deletions(-) rename src/Markup/Perspex.Markup.Xaml/{DataBinding => Binding}/IPerspexPropertyBinder.cs (92%) rename src/Markup/Perspex.Markup.Xaml/{DataBinding => Binding}/PerspexPropertyBinder.cs (97%) rename src/Markup/Perspex.Markup.Xaml/{DataBinding => Binding}/SourceBindingEndpoint.cs (94%) rename src/Markup/Perspex.Markup.Xaml/{DataBinding => Binding}/TargetBindingEndpoint.cs (91%) rename src/Markup/Perspex.Markup.Xaml/{DataBinding => Binding}/XamlBinding.cs (97%) rename src/Markup/Perspex.Markup.Xaml/{DataBinding => Binding}/XamlBindingDefinition.cs (94%) diff --git a/src/Markup/Perspex.Markup.Xaml/DataBinding/IPerspexPropertyBinder.cs b/src/Markup/Perspex.Markup.Xaml/Binding/IPerspexPropertyBinder.cs similarity index 92% rename from src/Markup/Perspex.Markup.Xaml/DataBinding/IPerspexPropertyBinder.cs rename to src/Markup/Perspex.Markup.Xaml/Binding/IPerspexPropertyBinder.cs index 40d349e00d..a98f32e7f1 100644 --- a/src/Markup/Perspex.Markup.Xaml/DataBinding/IPerspexPropertyBinder.cs +++ b/src/Markup/Perspex.Markup.Xaml/Binding/IPerspexPropertyBinder.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using OmniXaml.TypeConversion; -namespace Perspex.Markup.Xaml.DataBinding +namespace Perspex.Markup.Xaml.Binding { public interface IPerspexPropertyBinder { diff --git a/src/Markup/Perspex.Markup.Xaml/DataBinding/PerspexPropertyBinder.cs b/src/Markup/Perspex.Markup.Xaml/Binding/PerspexPropertyBinder.cs similarity index 97% rename from src/Markup/Perspex.Markup.Xaml/DataBinding/PerspexPropertyBinder.cs rename to src/Markup/Perspex.Markup.Xaml/Binding/PerspexPropertyBinder.cs index 916e3edd85..b0e2029ee0 100644 --- a/src/Markup/Perspex.Markup.Xaml/DataBinding/PerspexPropertyBinder.cs +++ b/src/Markup/Perspex.Markup.Xaml/Binding/PerspexPropertyBinder.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.Linq; using OmniXaml.TypeConversion; -namespace Perspex.Markup.Xaml.DataBinding +namespace Perspex.Markup.Xaml.Binding { public class PerspexPropertyBinder : IPerspexPropertyBinder { 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/DataBinding/XamlBinding.cs b/src/Markup/Perspex.Markup.Xaml/Binding/XamlBinding.cs similarity index 97% rename from src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBinding.cs rename to src/Markup/Perspex.Markup.Xaml/Binding/XamlBinding.cs index 330a835fc6..8204b55d17 100644 --- a/src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBinding.cs +++ b/src/Markup/Perspex.Markup.Xaml/Binding/XamlBinding.cs @@ -6,7 +6,7 @@ using System.Reactive.Linq; using OmniXaml.TypeConversion; using Perspex.Markup.Binding; -namespace Perspex.Markup.Xaml.DataBinding +namespace Perspex.Markup.Xaml.Binding { public class XamlBinding { diff --git a/src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBindingDefinition.cs b/src/Markup/Perspex.Markup.Xaml/Binding/XamlBindingDefinition.cs similarity index 94% rename from src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBindingDefinition.cs rename to src/Markup/Perspex.Markup.Xaml/Binding/XamlBindingDefinition.cs index 4cbd3bfe1e..da0c6d633e 100644 --- a/src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBindingDefinition.cs +++ b/src/Markup/Perspex.Markup.Xaml/Binding/XamlBindingDefinition.cs @@ -3,7 +3,7 @@ using Perspex.Controls; -namespace Perspex.Markup.Xaml.DataBinding +namespace Perspex.Markup.Xaml.Binding { public class XamlBindingDefinition { diff --git a/src/Markup/Perspex.Markup.Xaml/Context/PerspexTypeRepository.cs b/src/Markup/Perspex.Markup.Xaml/Context/PerspexTypeRepository.cs index 4d32ee8658..f0dcf0a444 100644 --- a/src/Markup/Perspex.Markup.Xaml/Context/PerspexTypeRepository.cs +++ b/src/Markup/Perspex.Markup.Xaml/Context/PerspexTypeRepository.cs @@ -5,7 +5,7 @@ using System; using Glass; using OmniXaml; using OmniXaml.Typing; -using Perspex.Markup.Xaml.DataBinding; +using Perspex.Markup.Xaml.Binding; namespace Perspex.Markup.Xaml.Context { diff --git a/src/Markup/Perspex.Markup.Xaml/Context/PerspexWiringContext.cs b/src/Markup/Perspex.Markup.Xaml/Context/PerspexWiringContext.cs index 933a39dbe4..c5373e031a 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; diff --git a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMember.cs b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMember.cs index dc4c4cbde2..172911a6f2 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; diff --git a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs index f978ecaa69..908fcd22fe 100644 --- a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs +++ b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs @@ -7,7 +7,7 @@ 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 diff --git a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlType.cs b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlType.cs index 827f6f3827..29de09c241 100644 --- a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlType.cs +++ b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlType.cs @@ -4,7 +4,7 @@ using System; using OmniXaml; using OmniXaml.Typing; -using Perspex.Markup.Xaml.DataBinding; +using Perspex.Markup.Xaml.Binding; namespace Perspex.Markup.Xaml.Context { diff --git a/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs index 55f2b1ee4a..5c124de956 100644 --- a/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs @@ -4,7 +4,7 @@ using System.Linq; using OmniXaml; using Perspex.Controls; -using Perspex.Markup.Xaml.DataBinding; +using Perspex.Markup.Xaml.Binding; namespace Perspex.Markup.Xaml.MarkupExtensions { diff --git a/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj b/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj index a117474005..7bfc7dc51a 100644 --- a/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj +++ b/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj @@ -38,6 +38,12 @@ Properties\SharedAssemblyInfo.cs + + + + + + @@ -224,12 +230,6 @@ - - - - - - diff --git a/src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs index e66d12ba75..23955b7682 100644 --- a/src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs +++ b/src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs @@ -6,7 +6,7 @@ using System.Collections; using OmniXaml.Attributes; using Perspex.Controls; using Perspex.Controls.Templates; -using Perspex.Markup.Xaml.DataBinding; +using Perspex.Markup.Xaml.Binding; namespace Perspex.Markup.Xaml.Templates { diff --git a/tests/Perspex.Markup.Xaml.UnitTests/BinderTest.cs b/tests/Perspex.Markup.Xaml.UnitTests/BinderTest.cs index 4a421bce2f..a39f4b0667 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/BinderTest.cs +++ b/tests/Perspex.Markup.Xaml.UnitTests/BinderTest.cs @@ -3,7 +3,7 @@ using System; using Moq; -using Perspex.Markup.Xaml.DataBinding; +using Perspex.Markup.Xaml.Binding; using OmniXaml.TypeConversion; using Xunit; diff --git a/tests/Perspex.Markup.Xaml.UnitTests/BindingDefinitionBuilder.cs b/tests/Perspex.Markup.Xaml.UnitTests/BindingDefinitionBuilder.cs index 985850e3e9..7e986201e3 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/BindingDefinitionBuilder.cs +++ b/tests/Perspex.Markup.Xaml.UnitTests/BindingDefinitionBuilder.cs @@ -2,7 +2,7 @@ // 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; +using Perspex.Markup.Xaml.Binding; namespace Perspex.Xaml.Base.UnitTest { diff --git a/tests/Perspex.Markup.Xaml.UnitTests/XamlBindingTest.cs b/tests/Perspex.Markup.Xaml.UnitTests/XamlBindingTest.cs index fcd90f6333..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; From 4f09de4dfe419c18c2a36ad743a4b16f90dddf46 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 2 Oct 2015 13:42:12 +0200 Subject: [PATCH 36/72] Started adding XamlBinding unit tests. --- .../Binding/XamlBinding.cs | 26 ++++- src/Perspex.Base/IObservablePropertyBag.cs | 38 ++++++ .../Binding/XamlBindingTests.cs | 109 ++++++++++++++++++ .../Perspex.Markup.Xaml.UnitTests.csproj | 1 + .../SelectorTests_Child.cs | 11 ++ .../SelectorTests_Descendent.cs | 11 ++ .../TestControlBase.cs | 11 ++ .../TestTemplatedControl.cs | 11 ++ 8 files changed, 212 insertions(+), 6 deletions(-) create mode 100644 tests/Perspex.Markup.Xaml.UnitTests/Binding/XamlBindingTests.cs diff --git a/src/Markup/Perspex.Markup.Xaml/Binding/XamlBinding.cs b/src/Markup/Perspex.Markup.Xaml/Binding/XamlBinding.cs index 8204b55d17..1bd2e86fd5 100644 --- a/src/Markup/Perspex.Markup.Xaml/Binding/XamlBinding.cs +++ b/src/Markup/Perspex.Markup.Xaml/Binding/XamlBinding.cs @@ -3,6 +3,7 @@ using System; using System.Reactive.Linq; +using System.Reactive.Subjects; using OmniXaml.TypeConversion; using Perspex.Markup.Binding; @@ -12,12 +13,16 @@ namespace Perspex.Markup.Xaml.Binding { private readonly ITypeConverterProvider _typeConverterProvider; + public XamlBinding() + { + } + public XamlBinding(ITypeConverterProvider typeConverterProvider) { _typeConverterProvider = typeConverterProvider; } - public PerspexObject Target { get; set; } + public IObservablePropertyBag Target { get; set; } public PerspexProperty TargetProperty { get; set; } @@ -28,6 +33,11 @@ namespace Perspex.Markup.Xaml.Binding public BindingMode BindingMode { get; set; } public void Bind() + { + Bind(new ExpressionSubject(CreateExpressionObserver())); + } + + public ExpressionObserver CreateExpressionObserver() { var path = SourcePropertyPath; var source = Source; @@ -42,23 +52,27 @@ namespace Perspex.Markup.Xaml.Binding source = Target; } - var observable = new ExpressionObserver(source, path); - var mode = BindingMode == BindingMode.Default ? + return new ExpressionObserver(source, path); + } + + internal void Bind(ISubject subject) + { + var mode = BindingMode == BindingMode.Default ? TargetProperty.DefaultBindingMode : BindingMode; switch (mode) { case BindingMode.Default: case BindingMode.OneWay: - Target.Bind(TargetProperty, observable.Select(x => x.Value)); + Target.Bind(TargetProperty, subject); break; case BindingMode.TwoWay: - Target.BindTwoWay(TargetProperty, new ExpressionSubject(observable)); + Target.BindTwoWay(TargetProperty, subject); break; case BindingMode.OneTime: throw new NotImplementedException(); case BindingMode.OneWayToSource: - Target.GetObservable(TargetProperty).Subscribe(new ExpressionSubject(observable)); + Target.GetObservable(TargetProperty).Subscribe(subject); break; } } 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/tests/Perspex.Markup.Xaml.UnitTests/Binding/XamlBindingTests.cs b/tests/Perspex.Markup.Xaml.UnitTests/Binding/XamlBindingTests.cs new file mode 100644 index 0000000000..7955438b0b --- /dev/null +++ b/tests/Perspex.Markup.Xaml.UnitTests/Binding/XamlBindingTests.cs @@ -0,0 +1,109 @@ +// 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 + { + Target = target.Object, + TargetProperty = TextBox.TextProperty, + SourcePropertyPath = "Foo", + BindingMode = BindingMode.OneWay, + }; + + binding.Bind(); + + 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 + { + Target = target.Object, + TargetProperty = TextBox.TextProperty, + SourcePropertyPath = "Foo", + BindingMode = BindingMode.TwoWay, + }; + + binding.Bind(); + + target.Verify(x => x.BindTwoWay( + TextBox.TextProperty, + It.IsAny>(), + 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 + { + Target = target.Object, + TargetProperty = TextBox.TextProperty, + SourcePropertyPath = "Foo", + BindingMode = BindingMode.OneWayToSource, + }; + + binding.Bind(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 + { + Target = target.Object, + TargetProperty = TextBox.TextProperty, + SourcePropertyPath = "Foo", + }; + + binding.Bind(); + + // 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((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/Perspex.Markup.Xaml.UnitTests.csproj b/tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj index 6c851a1e87..e014a04a48 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj +++ b/tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj @@ -89,6 +89,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(); + } } } From 1064fbd18683f89521e5a85e0b15e9df316f5ac1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 2 Oct 2015 14:41:51 +0200 Subject: [PATCH 37/72] Implement OneTime binding. --- samples/BindingTest/MainWindow.paml | 8 ++--- .../Binding/XamlBinding.cs | 27 ++++++---------- .../Binding/XamlBindingTests.cs | 32 +++++++++++++++++++ 3 files changed, 46 insertions(+), 21 deletions(-) diff --git a/samples/BindingTest/MainWindow.paml b/samples/BindingTest/MainWindow.paml index 38e7c14d40..3ac2621738 100644 --- a/samples/BindingTest/MainWindow.paml +++ b/samples/BindingTest/MainWindow.paml @@ -1,9 +1,9 @@  - - - - + + + + \ No newline at end of file diff --git a/src/Markup/Perspex.Markup.Xaml/Binding/XamlBinding.cs b/src/Markup/Perspex.Markup.Xaml/Binding/XamlBinding.cs index 1bd2e86fd5..3e3e978f7c 100644 --- a/src/Markup/Perspex.Markup.Xaml/Binding/XamlBinding.cs +++ b/src/Markup/Perspex.Markup.Xaml/Binding/XamlBinding.cs @@ -5,6 +5,7 @@ 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 @@ -26,8 +27,6 @@ namespace Perspex.Markup.Xaml.Binding public PerspexProperty TargetProperty { get; set; } - public object Source { get; set; } - public string SourcePropertyPath { get; set; } public BindingMode BindingMode { get; set; } @@ -39,20 +38,10 @@ namespace Perspex.Markup.Xaml.Binding public ExpressionObserver CreateExpressionObserver() { - var path = SourcePropertyPath; - var source = Source; - - if (source == null) - { - if (!string.IsNullOrWhiteSpace(path)) - { - path = "DataContext." + path; - } - - source = Target; - } - - return new ExpressionObserver(source, path); + var result = new ExpressionObserver(null, SourcePropertyPath); + var dataContext = Target.GetObservable(Control.DataContextProperty); + dataContext.Subscribe(x => result.Root = x); + return result; } internal void Bind(ISubject subject) @@ -70,7 +59,11 @@ namespace Perspex.Markup.Xaml.Binding Target.BindTwoWay(TargetProperty, subject); break; case BindingMode.OneTime: - throw new NotImplementedException(); + Target.GetObservable(Control.DataContextProperty).Subscribe(dataContext => + { + subject.Take(1).Subscribe(x => Target.SetValue(TargetProperty, x)); + }); + break; case BindingMode.OneWayToSource: Target.GetObservable(TargetProperty).Subscribe(subject); break; diff --git a/tests/Perspex.Markup.Xaml.UnitTests/Binding/XamlBindingTests.cs b/tests/Perspex.Markup.Xaml.UnitTests/Binding/XamlBindingTests.cs index 7955438b0b..321df1b475 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/Binding/XamlBindingTests.cs +++ b/tests/Perspex.Markup.Xaml.UnitTests/Binding/XamlBindingTests.cs @@ -53,6 +53,37 @@ namespace Perspex.Markup.Xaml.UnitTests.Binding 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 + { + Target = target.Object, + TargetProperty = TextBox.TextProperty, + SourcePropertyPath = "Foo", + BindingMode = BindingMode.OneTime, + }; + + binding.Bind(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() { @@ -101,6 +132,7 @@ namespace Perspex.Markup.Xaml.UnitTests.Binding 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; From 0c837f87a89dfc953bd17af2153456aa1ade018e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 2 Oct 2015 15:51:33 +0200 Subject: [PATCH 38/72] Add demo of binding to array index. Tried to add a button to shuffle ObservableCollection, but Command binding doesn't seem to work. --- samples/BindingTest/App.cs | 1 - samples/BindingTest/BindingTest.csproj | 17 +++++++++++++ samples/BindingTest/MainWindow.paml | 18 +++++++++----- .../ViewModels/MainWindowViewModel.cs | 24 ++++++++++++++++++- samples/BindingTest/ViewModels/TestItem.cs | 15 ++++++++++++ samples/BindingTest/packages.config | 5 ++++ src/Markup/Perspex.Markup.Xaml/OmniXAML | 2 +- src/Perspex.Base/PerspexObject.cs | 2 +- 8 files changed, 74 insertions(+), 10 deletions(-) create mode 100644 samples/BindingTest/ViewModels/TestItem.cs diff --git a/samples/BindingTest/App.cs b/samples/BindingTest/App.cs index 79222817cd..8399166015 100644 --- a/samples/BindingTest/App.cs +++ b/samples/BindingTest/App.cs @@ -18,7 +18,6 @@ namespace BindingTest Log.Logger = new LoggerConfiguration() .Filter.ByIncludingOnly(Matching.WithProperty("Area", "Property")) - .Filter.ByIncludingOnly(Matching.WithProperty("Property", x => x == "Text")) .MinimumLevel.Verbose() .WriteTo.Trace(outputTemplate: "[{Id:X8}] [{SourceContext}] {Message}") .CreateLogger(); diff --git a/samples/BindingTest/BindingTest.csproj b/samples/BindingTest/BindingTest.csproj index f98cc1dd08..62914fdc13 100644 --- a/samples/BindingTest/BindingTest.csproj +++ b/samples/BindingTest/BindingTest.csproj @@ -50,6 +50,22 @@ + + ..\..\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 + @@ -64,6 +80,7 @@ + diff --git a/samples/BindingTest/MainWindow.paml b/samples/BindingTest/MainWindow.paml index 3ac2621738..e42dd640e0 100644 --- a/samples/BindingTest/MainWindow.paml +++ b/samples/BindingTest/MainWindow.paml @@ -1,9 +1,15 @@  - - - - - - + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/BindingTest/ViewModels/MainWindowViewModel.cs b/samples/BindingTest/ViewModels/MainWindowViewModel.cs index e8ba113eb8..59e4b42f3e 100644 --- a/samples/BindingTest/ViewModels/MainWindowViewModel.cs +++ b/samples/BindingTest/ViewModels/MainWindowViewModel.cs @@ -1,4 +1,6 @@ -using ReactiveUI; +using System; +using System.Collections.ObjectModel; +using ReactiveUI; namespace BindingTest.ViewModels { @@ -6,6 +8,26 @@ namespace BindingTest.ViewModels { private string _simpleBinding = "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[r.Next(Items.Count)] = Items[r.Next(Items.Count)]; + }); + } + + public ObservableCollection Items { get; } + public ReactiveCommand ShuffleItems { get; } + public string SimpleBinding { get { return _simpleBinding; } 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 index 6a1eddec98..2bac0ad760 100644 --- a/samples/BindingTest/packages.config +++ b/samples/BindingTest/packages.config @@ -1,5 +1,10 @@  + + + + + \ No newline at end of file diff --git a/src/Markup/Perspex.Markup.Xaml/OmniXAML b/src/Markup/Perspex.Markup.Xaml/OmniXAML index 49e6ec001f..c890902fba 160000 --- a/src/Markup/Perspex.Markup.Xaml/OmniXAML +++ b/src/Markup/Perspex.Markup.Xaml/OmniXAML @@ -1 +1 @@ -Subproject commit 49e6ec001f5873cf2290e0bc1f6f06ca9b9cf808 +Subproject commit c890902fbaecb4752ed8e9fde2571396794fe06d diff --git a/src/Perspex.Base/PerspexObject.cs b/src/Perspex.Base/PerspexObject.cs index 0c27fe9453..4d0ec855fd 100644 --- a/src/Perspex.Base/PerspexObject.cs +++ b/src/Perspex.Base/PerspexObject.cs @@ -187,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) From 3730d6067fe064d6f80e2de9405065bbcb1c5321 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 2 Oct 2015 16:01:06 +0200 Subject: [PATCH 39/72] Add workaround for ReactiveCommand. --- src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs b/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs index c1a45d8784..3bdfb2ff3a 100644 --- a/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs +++ b/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs @@ -7,6 +7,7 @@ using System.Reactive.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; +using System.Windows.Input; namespace Perspex.Markup.Binding { @@ -107,10 +108,12 @@ namespace Perspex.Markup.Binding { var value = _propertyInfo.GetValue(target); var observable = value as IObservable; + var command = value as ICommand; var task = value as Task; bool set = false; - if (observable != null) + // ReactiveCommand is an IObservable but we want to bind to it, not its value. + if (observable != null && command == null) { CurrentValue = ExpressionValue.None; set = true; From 3fee7587bc3c3943836f23f5e2420f2eb985b03a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 2 Oct 2015 16:01:39 +0200 Subject: [PATCH 40/72] Make the shuffle more noticable. --- samples/BindingTest/ViewModels/MainWindowViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/BindingTest/ViewModels/MainWindowViewModel.cs b/samples/BindingTest/ViewModels/MainWindowViewModel.cs index 59e4b42f3e..d9647d0ee2 100644 --- a/samples/BindingTest/ViewModels/MainWindowViewModel.cs +++ b/samples/BindingTest/ViewModels/MainWindowViewModel.cs @@ -21,7 +21,7 @@ namespace BindingTest.ViewModels ShuffleItems.Subscribe(_ => { var r = new Random(); - Items[r.Next(Items.Count)] = Items[r.Next(Items.Count)]; + Items[1] = Items[r.Next(Items.Count)]; }); } From ed4f46a6cf88f3c8c759b694c02a652705f24804 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 2 Oct 2015 18:10:01 +0200 Subject: [PATCH 41/72] Don't throw when binding produces invalid value. --- src/Perspex.Base/PriorityValue.cs | 26 +++++++++---------- .../PerspexObjectTests_Binding.cs | 9 +++---- 2 files changed, 15 insertions(+), 20 deletions(-) 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/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] From d3e374d0c6c4f455147c209520f6d6cec88988e2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 2 Oct 2015 18:10:47 +0200 Subject: [PATCH 42/72] Update OmniXAML --- src/Markup/Perspex.Markup.Xaml/OmniXAML | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Markup/Perspex.Markup.Xaml/OmniXAML b/src/Markup/Perspex.Markup.Xaml/OmniXAML index c890902fba..708f8fdf02 160000 --- a/src/Markup/Perspex.Markup.Xaml/OmniXAML +++ b/src/Markup/Perspex.Markup.Xaml/OmniXAML @@ -1 +1 @@ -Subproject commit c890902fbaecb4752ed8e9fde2571396794fe06d +Subproject commit 708f8fdf0236029b3f8e87961390cc726260ade9 From a5ec3588c531477f43c8b35c38fb8ce4e4b4a725 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 2 Oct 2015 18:11:21 +0200 Subject: [PATCH 43/72] Test binding negation in BindingTest. --- samples/BindingTest/MainWindow.paml | 17 ++++++++++++----- .../ViewModels/MainWindowViewModel.cs | 15 +++++++++++---- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/samples/BindingTest/MainWindow.paml b/samples/BindingTest/MainWindow.paml index e42dd640e0..739651b21e 100644 --- a/samples/BindingTest/MainWindow.paml +++ b/samples/BindingTest/MainWindow.paml @@ -1,15 +1,22 @@  - - - - - + + + + + + + + + + !BooleanString + !!BooleanString + \ No newline at end of file diff --git a/samples/BindingTest/ViewModels/MainWindowViewModel.cs b/samples/BindingTest/ViewModels/MainWindowViewModel.cs index d9647d0ee2..f62add3712 100644 --- a/samples/BindingTest/ViewModels/MainWindowViewModel.cs +++ b/samples/BindingTest/ViewModels/MainWindowViewModel.cs @@ -6,7 +6,8 @@ namespace BindingTest.ViewModels { public class MainWindowViewModel : ReactiveObject { - private string _simpleBinding = "Simple Binding"; + private string _booleanString = "True"; + private string _stringValue = "Simple Binding"; public MainWindowViewModel() { @@ -28,10 +29,16 @@ namespace BindingTest.ViewModels public ObservableCollection Items { get; } public ReactiveCommand ShuffleItems { get; } - public string SimpleBinding + public string BooleanString { - get { return _simpleBinding; } - set { this.RaiseAndSetIfChanged(ref _simpleBinding, value); } + get { return _booleanString; } + set { this.RaiseAndSetIfChanged(ref _booleanString, value); } + } + + public string StringValue + { + get { return _stringValue; } + set { this.RaiseAndSetIfChanged(ref _stringValue, value); } } } } From 9aa3c18c58cb031cc4133ff79629b84ccc3ea63e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 2 Oct 2015 18:20:39 +0200 Subject: [PATCH 44/72] Fixed shuffle. --- samples/BindingTest/ViewModels/MainWindowViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/BindingTest/ViewModels/MainWindowViewModel.cs b/samples/BindingTest/ViewModels/MainWindowViewModel.cs index f62add3712..8722568aee 100644 --- a/samples/BindingTest/ViewModels/MainWindowViewModel.cs +++ b/samples/BindingTest/ViewModels/MainWindowViewModel.cs @@ -22,7 +22,7 @@ namespace BindingTest.ViewModels ShuffleItems.Subscribe(_ => { var r = new Random(); - Items[1] = Items[r.Next(Items.Count)]; + Items.Move(r.Next(Items.Count), 1); }); } From 14ca308900acd8eaa2fb7b253f04ba38e017a9a6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 2 Oct 2015 18:31:08 +0200 Subject: [PATCH 45/72] Log binding path. --- .../Binding/ExpressionObserver.cs | 11 ++++++++- .../Binding/ExpressionSubject.cs | 5 +++- .../Perspex.Markup/Perspex.Markup.csproj | 6 +++++ src/Perspex.Base/PerspexObject.cs | 24 +++++++++++++------ .../Perspex.Markup.UnitTests.csproj | 4 ++++ 5 files changed, 41 insertions(+), 9 deletions(-) diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs b/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs index fc7a4b2663..ff98e82aeb 100644 --- a/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs +++ b/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs @@ -10,7 +10,7 @@ namespace Perspex.Markup.Binding /// /// Observes and sets the value of an expression on an object. /// - public class ExpressionObserver : ObservableBase + public class ExpressionObserver : ObservableBase, IDescription { private object _root; private int _count; @@ -25,6 +25,7 @@ namespace Perspex.Markup.Binding { _root = root; _node = ExpressionNodeBuilder.Build(expression); + Expression = expression; } /// @@ -49,6 +50,11 @@ namespace Perspex.Markup.Binding } } + /// + /// Gets the expression being observed. + /// + public string Expression { get; } + /// /// Gets or sets the root object that the expression is being observed on. /// @@ -73,6 +79,9 @@ namespace Perspex.Markup.Binding } } + /// + string IDescription.Description => Expression; + /// protected override IDisposable SubscribeCore(IObserver observer) { diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionSubject.cs b/src/Markup/Perspex.Markup/Binding/ExpressionSubject.cs index cd076c9ab7..ac5700e512 100644 --- a/src/Markup/Perspex.Markup/Binding/ExpressionSubject.cs +++ b/src/Markup/Perspex.Markup/Binding/ExpressionSubject.cs @@ -10,7 +10,7 @@ namespace Perspex.Markup.Binding /// /// Turns an into a subject that can be bound two-ways. /// - public class ExpressionSubject : ISubject + public class ExpressionSubject : ISubject, IDescription { private ExpressionObserver _inner; @@ -23,6 +23,9 @@ namespace Perspex.Markup.Binding _inner = inner; } + /// + string IDescription.Description => _inner.Expression; + /// public void OnCompleted() { diff --git a/src/Markup/Perspex.Markup/Perspex.Markup.csproj b/src/Markup/Perspex.Markup/Perspex.Markup.csproj index d22201ee9f..a7e6bf61cd 100644 --- a/src/Markup/Perspex.Markup/Perspex.Markup.csproj +++ b/src/Markup/Perspex.Markup/Perspex.Markup.csproj @@ -71,6 +71,12 @@ + + + {b09b78d8-9b26-48b0-9149-d64a2f120f3f} + Perspex.Base + + + --> + diff --git a/src/Markup/Perspex.Markup.Xaml/Binding/PerspexPropertyBinder.cs b/src/Markup/Perspex.Markup.Xaml/Binding/PerspexPropertyBinder.cs index b0e2029ee0..9e4eee4b84 100644 --- a/src/Markup/Perspex.Markup.Xaml/Binding/PerspexPropertyBinder.cs +++ b/src/Markup/Perspex.Markup.Xaml/Binding/PerspexPropertyBinder.cs @@ -22,38 +22,41 @@ namespace Perspex.Markup.Xaml.Binding public XamlBinding GetBinding(PerspexObject po, PerspexProperty pp) { - return _bindings.First(xamlBinding => xamlBinding.Target == po && xamlBinding.TargetProperty == pp); + throw new NotImplementedException(); + //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; + throw new NotImplementedException(); + //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; + throw new NotImplementedException(); + //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/Binding/XamlBinding.cs b/src/Markup/Perspex.Markup.Xaml/Binding/XamlBinding.cs index 3e3e978f7c..3bf709e928 100644 --- a/src/Markup/Perspex.Markup.Xaml/Binding/XamlBinding.cs +++ b/src/Markup/Perspex.Markup.Xaml/Binding/XamlBinding.cs @@ -23,49 +23,46 @@ namespace Perspex.Markup.Xaml.Binding _typeConverterProvider = typeConverterProvider; } - public IObservablePropertyBag Target { get; set; } - - public PerspexProperty TargetProperty { get; set; } - public string SourcePropertyPath { get; set; } public BindingMode BindingMode { get; set; } - public void Bind() + public void Bind(IObservablePropertyBag instance, PerspexProperty property) { - Bind(new ExpressionSubject(CreateExpressionObserver())); + var subject = new ExpressionSubject(CreateExpressionObserver(instance)); + Bind(instance, property, subject); } - public ExpressionObserver CreateExpressionObserver() + public ExpressionObserver CreateExpressionObserver(IObservablePropertyBag instance) { var result = new ExpressionObserver(null, SourcePropertyPath); - var dataContext = Target.GetObservable(Control.DataContextProperty); + var dataContext = instance.GetObservable(Control.DataContextProperty); dataContext.Subscribe(x => result.Root = x); return result; } - internal void Bind(ISubject subject) + internal void Bind(IObservablePropertyBag target, PerspexProperty property, ISubject subject) { var mode = BindingMode == BindingMode.Default ? - TargetProperty.DefaultBindingMode : BindingMode; + property.DefaultBindingMode : BindingMode; switch (mode) { case BindingMode.Default: case BindingMode.OneWay: - Target.Bind(TargetProperty, subject); + target.Bind(property, subject); break; case BindingMode.TwoWay: - Target.BindTwoWay(TargetProperty, subject); + target.BindTwoWay(property, subject); break; case BindingMode.OneTime: - Target.GetObservable(Control.DataContextProperty).Subscribe(dataContext => + target.GetObservable(Control.DataContextProperty).Subscribe(dataContext => { - subject.Take(1).Subscribe(x => Target.SetValue(TargetProperty, x)); + subject.Take(1).Subscribe(x => target.SetValue(property, x)); }); break; case BindingMode.OneWayToSource: - Target.GetObservable(TargetProperty).Subscribe(subject); + target.GetObservable(property).Subscribe(subject); break; } } diff --git a/src/Markup/Perspex.Markup.Xaml/Binding/XamlBindingDefinition.cs b/src/Markup/Perspex.Markup.Xaml/Binding/XamlBindingDefinition.cs index da0c6d633e..c571036d4d 100644 --- a/src/Markup/Perspex.Markup.Xaml/Binding/XamlBindingDefinition.cs +++ b/src/Markup/Perspex.Markup.Xaml/Binding/XamlBindingDefinition.cs @@ -8,19 +8,13 @@ namespace Perspex.Markup.Xaml.Binding public class XamlBindingDefinition { public XamlBindingDefinition( - Control target, - PerspexProperty targetProperty, string sourcePropertyPath, BindingMode bindingMode) { - Target = target; - TargetProperty = targetProperty; SourcePropertyPath = sourcePropertyPath; BindingMode = bindingMode; } - public Control Target { get; } - public PerspexProperty TargetProperty { get; } public string SourcePropertyPath { get; } public BindingMode BindingMode { get; } } diff --git a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs index 908fcd22fe..1bb324c5a8 100644 --- a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs +++ b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.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.Linq; using System.Reactive.Linq; using Glass; using OmniXaml.ObjectAssembler; @@ -27,7 +28,7 @@ namespace Perspex.Markup.Xaml.Context { if (value is XamlBindingDefinition) { - HandleXamlBindingDefinition((XamlBindingDefinition)value); + HandleXamlBindingDefinition(instance, (XamlBindingDefinition)value); } else if (IsPerspexProperty) { @@ -54,17 +55,32 @@ namespace Perspex.Markup.Xaml.Context po.SetValue(pp, value); } - private void HandleXamlBindingDefinition(XamlBindingDefinition def) + private void HandleXamlBindingDefinition(object instance, XamlBindingDefinition def) { + var perspexObject = instance as PerspexObject; + + 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(_propertyBinder.TypeConverterProvider) { BindingMode = def.BindingMode, SourcePropertyPath = def.SourcePropertyPath, - Target = def.Target, - TargetProperty = def.TargetProperty, }; - binding.Bind(); + binding.Bind(perspexObject, property); } private void BindToDataContextWhenItsSet(XamlBindingDefinition definition) diff --git a/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs index 5c124de956..7abc09ef89 100644 --- a/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs @@ -21,11 +21,7 @@ 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, Path, Mode); + return new XamlBindingDefinition(Path, Mode); } public string Path { get; set; } diff --git a/tests/Perspex.Markup.Xaml.UnitTests/BinderTest.cs b/tests/Perspex.Markup.Xaml.UnitTests/BinderTest.cs deleted file mode 100644 index a39f4b0667..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.Binding; -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 index 321df1b475..97e0dc7741 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/Binding/XamlBindingTests.cs +++ b/tests/Perspex.Markup.Xaml.UnitTests/Binding/XamlBindingTests.cs @@ -19,13 +19,11 @@ namespace Perspex.Markup.Xaml.UnitTests.Binding var target = CreateTarget(); var binding = new XamlBinding { - Target = target.Object, - TargetProperty = TextBox.TextProperty, SourcePropertyPath = "Foo", BindingMode = BindingMode.OneWay, }; - binding.Bind(); + binding.Bind(target.Object, TextBox.TextProperty); target.Verify(x => x.Bind( TextBox.TextProperty, @@ -39,13 +37,11 @@ namespace Perspex.Markup.Xaml.UnitTests.Binding var target = CreateTarget(); var binding = new XamlBinding { - Target = target.Object, - TargetProperty = TextBox.TextProperty, SourcePropertyPath = "Foo", BindingMode = BindingMode.TwoWay, }; - binding.Bind(); + binding.Bind(target.Object, TextBox.TextProperty); target.Verify(x => x.BindTwoWay( TextBox.TextProperty, @@ -61,13 +57,11 @@ namespace Perspex.Markup.Xaml.UnitTests.Binding var target = CreateTarget(dataContext: dataContext); var binding = new XamlBinding { - Target = target.Object, - TargetProperty = TextBox.TextProperty, SourcePropertyPath = "Foo", BindingMode = BindingMode.OneTime, }; - binding.Bind(expression); + binding.Bind(target.Object, TextBox.TextProperty, expression); target.Verify(x => x.SetValue( (PerspexProperty)TextBox.TextProperty, @@ -92,13 +86,11 @@ namespace Perspex.Markup.Xaml.UnitTests.Binding var target = CreateTarget(text: textObservable.Object); var binding = new XamlBinding { - Target = target.Object, - TargetProperty = TextBox.TextProperty, SourcePropertyPath = "Foo", BindingMode = BindingMode.OneWayToSource, }; - binding.Bind(expression.Object); + binding.Bind(target.Object, TextBox.TextProperty, expression.Object); textObservable.Verify(x => x.Subscribe(expression.Object)); } @@ -109,12 +101,10 @@ namespace Perspex.Markup.Xaml.UnitTests.Binding var target = CreateTarget(null); var binding = new XamlBinding { - Target = target.Object, - TargetProperty = TextBox.TextProperty, SourcePropertyPath = "Foo", }; - binding.Bind(); + binding.Bind(target.Object, TextBox.TextProperty); // Default for TextBox.Text is two-way. target.Verify(x => x.BindTwoWay( diff --git a/tests/Perspex.Markup.Xaml.UnitTests/BindingDefinitionBuilder.cs b/tests/Perspex.Markup.Xaml.UnitTests/BindingDefinitionBuilder.cs deleted file mode 100644 index 7e986201e3..0000000000 --- a/tests/Perspex.Markup.Xaml.UnitTests/BindingDefinitionBuilder.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. - -using Perspex.Controls; -using Perspex.Markup.Xaml.Binding; - -namespace Perspex.Xaml.Base.UnitTest -{ - public class BindingDefinitionBuilder - { - private readonly BindingMode _bindingMode; - private readonly string _sourcePropertyPath; - private Control _target; - - public BindingDefinitionBuilder() - { - _bindingMode = BindingMode.Default; - _sourcePropertyPath = string.Empty; - } - - public BindingDefinitionBuilder WithNullTarget() - { - _target = null; - return this; - } - - public XamlBindingDefinition Build() - { - return new XamlBindingDefinition( - bindingMode: _bindingMode, - sourcePropertyPath: _sourcePropertyPath, - target: _target, - targetProperty: null); - } - } -} \ No newline at end of file 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 e014a04a48..2ac180603f 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj +++ b/tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj @@ -88,7 +88,6 @@ - @@ -103,7 +102,6 @@ - From 92535c390448de1fff9e88d2c902de82fb014771 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 2 Oct 2015 19:52:39 +0200 Subject: [PATCH 50/72] Make TreeDataTemplate work. --- .../XamlTestApplication/Views/MainWindow.paml | 4 +- .../Context/PerspexObjectAssembler.cs | 1 + .../Context/PerspexXamlMemberValuePlugin.cs | 64 +++++++++++-------- .../Templates/TreeDataTemplate.cs | 8 ++- 4 files changed, 44 insertions(+), 33 deletions(-) diff --git a/samples/XamlTestApplication/Views/MainWindow.paml b/samples/XamlTestApplication/Views/MainWindow.paml index 9fe6a760a8..149948db44 100644 --- a/samples/XamlTestApplication/Views/MainWindow.paml +++ b/samples/XamlTestApplication/Views/MainWindow.paml @@ -70,14 +70,14 @@ - + diff --git a/src/Markup/Perspex.Markup.Xaml/Context/PerspexObjectAssembler.cs b/src/Markup/Perspex.Markup.Xaml/Context/PerspexObjectAssembler.cs index 06d78ab753..25df85a3cc 100644 --- a/src/Markup/Perspex.Markup.Xaml/Context/PerspexObjectAssembler.cs +++ b/src/Markup/Perspex.Markup.Xaml/Context/PerspexObjectAssembler.cs @@ -16,6 +16,7 @@ namespace Perspex.Markup.Xaml.Context { var mapping = new DeferredLoaderMapping(); 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/PerspexXamlMemberValuePlugin.cs b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs index 1bb324c5a8..c63bbc9a24 100644 --- a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs +++ b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using System.Reactive.Linq; +using System.Reflection; using Glass; using OmniXaml.ObjectAssembler; using OmniXaml.Typing; @@ -57,39 +58,46 @@ namespace Perspex.Markup.Xaml.Context private void HandleXamlBindingDefinition(object instance, XamlBindingDefinition def) { - var perspexObject = instance as PerspexObject; - - if (perspexObject == null) + if (_xamlMember.XamlType.UnderlyingType == typeof(XamlBindingDefinition)) { - throw new InvalidOperationException( - $"Cannot bind to an object of type '{instance.GetType()}"); - } + // TODO: This should search base classes. + var property = instance.GetType().GetTypeInfo().GetDeclaredProperty(_xamlMember.Name); - var property = perspexObject.GetRegisteredProperties() - .FirstOrDefault(x => x.Name == _xamlMember.Name); + if (property == null || !property.CanWrite) + { + throw new InvalidOperationException( + $"Cannot assign to '{_xamlMember.Name}' on '{instance.GetType()}"); + } - if (property == null) - { - throw new InvalidOperationException( - $"Cannot find '{_xamlMember.Name}' on '{instance.GetType()}"); + property.SetValue(instance, def); } - - var binding = new XamlBinding(_propertyBinder.TypeConverterProvider) + else { - BindingMode = def.BindingMode, - SourcePropertyPath = def.SourcePropertyPath, - }; - - binding.Bind(perspexObject, property); - } - - private void BindToDataContextWhenItsSet(XamlBindingDefinition definition) - { - // var target = definition.Target; - // var dataContext = target.DataContext; - - // var binding = _propertyBinder.GetBinding(target, definition.TargetProperty); - // binding.BindToDataContext(dataContext); + var perspexObject = instance as PerspexObject; + + 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(_propertyBinder.TypeConverterProvider) + { + BindingMode = def.BindingMode, + SourcePropertyPath = def.SourcePropertyPath, + }; + + binding.Bind(perspexObject, property); + } } // ReSharper disable once MemberCanBePrivate.Global diff --git a/src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs index 44b264897a..0868dae69f 100644 --- a/src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs +++ b/src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs @@ -3,9 +3,10 @@ using System; using System.Collections; -using OmniXaml.Attributes; +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 @@ -14,7 +15,7 @@ namespace Perspex.Markup.Xaml.Templates { public Type DataType { get; set; } public TemplateContent Content { get; set; } - public XamlBinding ItemsSource { get; set; } + public XamlBindingDefinition ItemsSource { get; set; } public bool Match(object data) { @@ -30,7 +31,8 @@ namespace Perspex.Markup.Xaml.Templates { if (ItemsSource != null) { - // TODO: Get value of ItemsSource here. + var obs = new ExpressionObserver(item, ItemsSource.SourcePropertyPath); + return obs.Take(1).Wait().Value as IEnumerable; } return null; From 521771d2543a031c36de41926d7f9c20e23c7eed Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 2 Oct 2015 20:24:17 +0200 Subject: [PATCH 51/72] Removed unused class/interface. --- .../Binding/IPerspexPropertyBinder.cs | 19 ------ .../Binding/PerspexPropertyBinder.cs | 62 ------------------- .../Context/PerspexTypeRepository.cs | 7 +-- .../Context/PerspexWiringContext.cs | 3 +- .../Context/PerspexXamlMember.cs | 8 +-- .../Context/PerspexXamlMemberValuePlugin.cs | 7 +-- .../Context/PerspexXamlType.cs | 10 +-- .../Perspex.Markup.Xaml.csproj | 2 - 8 files changed, 10 insertions(+), 108 deletions(-) delete mode 100644 src/Markup/Perspex.Markup.Xaml/Binding/IPerspexPropertyBinder.cs delete mode 100644 src/Markup/Perspex.Markup.Xaml/Binding/PerspexPropertyBinder.cs diff --git a/src/Markup/Perspex.Markup.Xaml/Binding/IPerspexPropertyBinder.cs b/src/Markup/Perspex.Markup.Xaml/Binding/IPerspexPropertyBinder.cs deleted file mode 100644 index a98f32e7f1..0000000000 --- a/src/Markup/Perspex.Markup.Xaml/Binding/IPerspexPropertyBinder.cs +++ /dev/null @@ -1,19 +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; -using OmniXaml.TypeConversion; - -namespace Perspex.Markup.Xaml.Binding -{ - public interface IPerspexPropertyBinder - { - ITypeConverterProvider TypeConverterProvider { get; } - - 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/Binding/PerspexPropertyBinder.cs b/src/Markup/Perspex.Markup.Xaml/Binding/PerspexPropertyBinder.cs deleted file mode 100644 index 9e4eee4b84..0000000000 --- a/src/Markup/Perspex.Markup.Xaml/Binding/PerspexPropertyBinder.cs +++ /dev/null @@ -1,62 +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.Binding -{ - public class PerspexPropertyBinder : IPerspexPropertyBinder - { - private readonly HashSet _bindings; - - public PerspexPropertyBinder(ITypeConverterProvider typeConverterProvider) - { - TypeConverterProvider = typeConverterProvider; - _bindings = new HashSet(); - } - - public ITypeConverterProvider TypeConverterProvider { get; } - - public XamlBinding GetBinding(PerspexObject po, PerspexProperty pp) - { - throw new NotImplementedException(); - //return _bindings.First(xamlBinding => xamlBinding.Target == po && xamlBinding.TargetProperty == pp); - } - - public IEnumerable GetBindings(PerspexObject source) - { - throw new NotImplementedException(); - //return from binding in _bindings - // where binding.Target == source - // select binding; - } - - public XamlBinding Create(XamlBindingDefinition xamlBinding) - { - throw new NotImplementedException(); - //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/Context/PerspexTypeRepository.cs b/src/Markup/Perspex.Markup.Xaml/Context/PerspexTypeRepository.cs index f0dcf0a444..44dda9b9f9 100644 --- a/src/Markup/Perspex.Markup.Xaml/Context/PerspexTypeRepository.cs +++ b/src/Markup/Perspex.Markup.Xaml/Context/PerspexTypeRepository.cs @@ -12,21 +12,18 @@ 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 e02512fe1e..3e5b5c24c2 100644 --- a/src/Markup/Perspex.Markup.Xaml/Context/PerspexWiringContext.cs +++ b/src/Markup/Perspex.Markup.Xaml/Context/PerspexWiringContext.cs @@ -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( diff --git a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMember.cs b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMember.cs index 172911a6f2..89979885f0 100644 --- a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMember.cs +++ b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMember.cs @@ -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 c63bbc9a24..fd7a786649 100644 --- a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs +++ b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs @@ -17,12 +17,11 @@ 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) @@ -90,7 +89,7 @@ namespace Perspex.Markup.Xaml.Context $"Cannot find '{_xamlMember.Name}' on '{instance.GetType()}"); } - var binding = new XamlBinding(_propertyBinder.TypeConverterProvider) + var binding = new XamlBinding { BindingMode = def.BindingMode, SourcePropertyPath = def.SourcePropertyPath, diff --git a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlType.cs b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlType.cs index 29de09c241..0d1fb218b1 100644 --- a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlType.cs +++ b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlType.cs @@ -10,22 +10,16 @@ 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/Perspex.Markup.Xaml.csproj b/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj index 7bfc7dc51a..987b630907 100644 --- a/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj +++ b/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj @@ -38,8 +38,6 @@ Properties\SharedAssemblyInfo.cs - - From 69d8468c61446254b364bae566b5700ef133e05f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 2 Oct 2015 20:36:08 +0200 Subject: [PATCH 52/72] Retarget OmniXAML to perspex fork. --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 4f67ed057f841aca4d200d591c86c892964a534e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 2 Oct 2015 20:42:13 +0200 Subject: [PATCH 53/72] Updated OmniXAML --- src/Markup/Perspex.Markup.Xaml/OmniXAML | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Markup/Perspex.Markup.Xaml/OmniXAML b/src/Markup/Perspex.Markup.Xaml/OmniXAML index 708f8fdf02..42b0e3b6ef 160000 --- a/src/Markup/Perspex.Markup.Xaml/OmniXAML +++ b/src/Markup/Perspex.Markup.Xaml/OmniXAML @@ -1 +1 @@ -Subproject commit 708f8fdf0236029b3f8e87961390cc726260ade9 +Subproject commit 42b0e3b6efc0905457120752be38a6000898fffa From df47a491e130d6b6397f54b6908c31918ffefcf0 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 2 Oct 2015 21:00:35 +0200 Subject: [PATCH 54/72] Fixed .sln. --- Perspex.sln | 1 + 1 file changed, 1 insertion(+) diff --git a/Perspex.sln b/Perspex.sln index 2c4c334a9c..6ab0be278c 100644 --- a/Perspex.sln +++ b/Perspex.sln @@ -101,6 +101,7 @@ 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 From 572ecfa521e3c4ab1f003afe7c09acf5f5162e50 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 2 Oct 2015 21:35:50 +0200 Subject: [PATCH 55/72] Added Perspex.Markup to NuGet package. --- nuget/build-version.ps1 | 2 ++ src/Markup/Perspex.Markup/Perspex.Markup.csproj | 1 + 2 files changed, 3 insertions(+) diff --git a/nuget/build-version.ps1 b/nuget/build-version.ps1 index 1cce68564d..f458b30ffd 100644 --- a/nuget/build-version.ps1 +++ b/nuget/build-version.ps1 @@ -28,6 +28,8 @@ Copy-Item ..\src\Perspex.Styling\bin\Release\Perspex.Styling.dll $lib Copy-Item ..\src\Perspex.Styling\bin\Release\Perspex.Styling.xml $lib Copy-Item ..\src\Perspex.Themes.Default\bin\Release\Perspex.Themes.Default.dll $lib Copy-Item ..\src\Perspex.Themes.Default\bin\Release\Perspex.Themes.Default.xml $lib +Copy-Item ..\src\Markup\Perspex.Markup\bin\Release\Perspex.Markup.dll $lib +Copy-Item ..\src\Markup\Perspex.Markup\bin\Release\Perspex.Markup.xml $lib Copy-Item ..\src\Markup\Perspex.Markup.Xaml\bin\Release\Perspex.Markup.Xaml.dll $lib Copy-Item ..\src\Markup\Perspex.Markup.Xaml\bin\Release\Perspex.Markup.Xaml.xml $lib Copy-Item ..\src\Perspex.HtmlRenderer\bin\Release\Perspex.HtmlRenderer.dll $lib diff --git a/src/Markup/Perspex.Markup/Perspex.Markup.csproj b/src/Markup/Perspex.Markup/Perspex.Markup.csproj index a7e6bf61cd..32fb2e3233 100644 --- a/src/Markup/Perspex.Markup/Perspex.Markup.csproj +++ b/src/Markup/Perspex.Markup/Perspex.Markup.csproj @@ -32,6 +32,7 @@ TRACE prompt 4 + bin\Release\Perspex.Markup.XML From c9d08fd69157815013e9640cfd4345b52dad33cc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 3 Oct 2015 02:06:13 +0200 Subject: [PATCH 56/72] Fix PerspexProperty.IsSet. --- src/Perspex.Base/PerspexObject.cs | 9 +++++- .../PerspexObjectTests_Metadata.cs | 29 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/Perspex.Base/PerspexObject.cs b/src/Perspex.Base/PerspexObject.cs index f0abeaf8fa..a08011d025 100644 --- a/src/Perspex.Base/PerspexObject.cs +++ b/src/Perspex.Base/PerspexObject.cs @@ -457,8 +457,15 @@ namespace Perspex public bool IsSet(PerspexProperty property) { Contract.Requires(property != null); + + PriorityValue value; + + if (_values.TryGetValue(property, out value)) + { + return value.Value != PerspexProperty.UnsetValue; + } - return _values.ContainsKey(property); + return false; } /// diff --git a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Metadata.cs b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Metadata.cs index b905f8b5ba..7fa816cf82 100644 --- a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Metadata.cs +++ b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Metadata.cs @@ -42,6 +42,35 @@ namespace Perspex.Base.UnitTests Assert.Equal(new[] { "Attached" }, names); } + [Fact] + public void IsSet_Returns_False_For_Unset_Property() + { + var target = new Class1(); + + Assert.False(target.IsSet(Class1.FooProperty)); + } + + [Fact] + public void IsSet_Returns_False_For_Set_Property() + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "foo"); + + Assert.True(target.IsSet(Class1.FooProperty)); + } + + [Fact] + public void IsSet_Returns_False_For_Cleared_Property() + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "foo"); + target.SetValue(Class1.FooProperty, PerspexProperty.UnsetValue); + + Assert.False(target.IsSet(Class1.FooProperty)); + } + private class Class1 : PerspexObject { public static readonly PerspexProperty FooProperty = From 3fc62506df5ea7d5918183614af8417b70924fc7 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 3 Oct 2015 10:31:14 +0300 Subject: [PATCH 57/72] Introduced DrawingContext class responsible for matrix transformation and push/pop order validation --- src/Gtk/Perspex.Cairo/Media/DrawingContext.cs | 74 ++++---- .../Media/Imaging/RenderTargetBitmapImpl.cs | 2 +- src/Gtk/Perspex.Cairo/RenderTarget.cs | 12 +- src/Perspex.Application/Application.cs | 2 +- src/Perspex.Controls/Border.cs | 2 +- src/Perspex.Controls/Image.cs | 2 +- src/Perspex.Controls/Panel.cs | 4 +- .../Presenters/TextPresenter.cs | 2 +- src/Perspex.Controls/Primitives/AccessText.cs | 2 +- src/Perspex.Controls/Shapes/Shape.cs | 2 +- src/Perspex.Controls/TextBlock.cs | 2 +- .../Adapters/GraphicsAdapter.cs | 4 +- src/Perspex.HtmlRenderer/HtmlContainer.cs | 2 +- src/Perspex.HtmlRenderer/HtmlControl.cs | 2 +- src/Perspex.SceneGraph/IVisual.cs | 4 +- .../Media/DrawingContext.cs | 160 ++++++++++++++++++ .../Media/IDrawingContext.cs | 19 +-- .../Media/Imaging/RenderTargetBitmap.cs | 2 +- .../Media/ValidatingDrawingContext.cs | 76 --------- .../Perspex.SceneGraph.csproj | 2 +- .../Platform/IRenderTarget.cs | 4 +- .../Rendering/RendererBase.cs | 4 +- src/Perspex.SceneGraph/Visual.cs | 4 +- .../Perspex.Direct2D1/Media/DrawingContext.cs | 59 +++---- .../Media/Imaging/RenderTargetBitmapImpl.cs | 2 +- src/Windows/Perspex.Direct2D1/RenderTarget.cs | 19 +-- 26 files changed, 274 insertions(+), 195 deletions(-) create mode 100644 src/Perspex.SceneGraph/Media/DrawingContext.cs delete mode 100644 src/Perspex.SceneGraph/Media/ValidatingDrawingContext.cs diff --git a/src/Gtk/Perspex.Cairo/Media/DrawingContext.cs b/src/Gtk/Perspex.Cairo/Media/DrawingContext.cs index f8c74f3f8f..3efb60e5c5 100644 --- a/src/Gtk/Perspex.Cairo/Media/DrawingContext.cs +++ b/src/Gtk/Perspex.Cairo/Media/DrawingContext.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections.Generic; using System.Linq; using System.Reactive.Disposables; using System.Runtime.InteropServices; @@ -16,7 +17,7 @@ namespace Perspex.Cairo.Media /// /// Draws using Direct2D1. /// - public class DrawingContext : IDrawingContext, IDisposable + public class DrawingContext : IDrawingContextImpl, IDisposable { /// /// The cairo context. @@ -30,7 +31,6 @@ namespace Perspex.Cairo.Media public DrawingContext(Cairo.Surface surface) { _context = new Cairo.Context(surface); - CurrentTransform = Matrix.Identity; } /// @@ -40,15 +40,23 @@ namespace Perspex.Cairo.Media public DrawingContext(Gdk.Drawable drawable) { _context = Gdk.CairoHelper.Create(drawable); - CurrentTransform = Matrix.Identity; } + + private Matrix _transform = Matrix.Identity; /// /// Gets the current transform of the drawing context. /// - public Matrix CurrentTransform + public Matrix Transform { - get; } + get { return _transform; } + set + { + _transform = value; + _context.Matrix = value.ToCairo(); + + } + } /// /// Ends a draw operation. @@ -131,29 +139,31 @@ namespace Perspex.Cairo.Media { var impl = geometry.PlatformImpl as StreamGeometryImpl; - using (var pop = PushTransform(impl.Transform)) + var oldMatrix = Transform; + Transform *= impl.Transform; + + + + if (brush != null) { _context.AppendPath(impl.Path); - - if (brush != null) + using (var b = SetBrush(brush, geometry.Bounds.Size)) { - using (var b = SetBrush(brush, geometry.Bounds.Size)) - { - if (pen != null) - _context.FillPreserve(); - else - _context.Fill(); - } + if (pen != null) + _context.FillPreserve(); + else + _context.Fill(); } } - if (pen != null) { - using (var p = SetPen(pen, geometry.Bounds.Size)) - { - _context.Stroke(); - } + _context.AppendPath(impl.Path); + using (var p = SetPen(pen, geometry.Bounds.Size)) + { + _context.Stroke(); + } } + Transform = oldMatrix; } /// @@ -208,31 +218,37 @@ namespace Perspex.Cairo.Media /// /// The clip rectangle. /// A disposable used to undo the clip rectangle. - public IDisposable PushClip(Rect clip) + public void PushClip(Rect clip) { _context.Save(); _context.Rectangle(clip.ToCairo()); _context.Clip(); + } - return Disposable.Create(() => _context.Restore()); + public void PopClip() + { + _context.Restore(); } + readonly Stack _opacityStack = new Stack(); + /// /// Pushes an opacity value. /// /// The opacity. /// A disposable used to undo the opacity. - public IDisposable PushOpacity(double opacity) + public void PushOpacity(double opacity) { - var tmp = opacityOverride; + _opacityStack.Push(opacityOverride); if (opacity < 1.0f) - opacityOverride = opacity; + opacityOverride *= opacity; - return Disposable.Create(() => - { - opacityOverride = tmp; - }); + } + + public void PopOpacity() + { + opacityOverride = _opacityStack.Pop(); } /// diff --git a/src/Gtk/Perspex.Cairo/Media/Imaging/RenderTargetBitmapImpl.cs b/src/Gtk/Perspex.Cairo/Media/Imaging/RenderTargetBitmapImpl.cs index 0b9183d928..4d8f05848a 100644 --- a/src/Gtk/Perspex.Cairo/Media/Imaging/RenderTargetBitmapImpl.cs +++ b/src/Gtk/Perspex.Cairo/Media/Imaging/RenderTargetBitmapImpl.cs @@ -39,7 +39,7 @@ namespace Perspex.Cairo.Media.Imaging Surface.WriteToPng(fileName); } - public IDrawingContext CreateDrawingContext() + public Perspex.Media.DrawingContext CreateDrawingContext() { return _renderTarget.CreateDrawingContext(); } diff --git a/src/Gtk/Perspex.Cairo/RenderTarget.cs b/src/Gtk/Perspex.Cairo/RenderTarget.cs index 333c558e8a..2daab33fbe 100644 --- a/src/Gtk/Perspex.Cairo/RenderTarget.cs +++ b/src/Gtk/Perspex.Cairo/RenderTarget.cs @@ -7,6 +7,7 @@ using Perspex.Cairo.Media; using Perspex.Media; using Perspex.Platform; using Perspex.Rendering; +using DrawingContext = Perspex.Media.DrawingContext; namespace Perspex.Cairo { @@ -50,12 +51,13 @@ namespace Perspex.Cairo /// /// Creates a cairo surface that targets a platform-specific resource. /// - /// A surface wrapped in an . - public IDrawingContext CreateDrawingContext() + /// A surface wrapped in an . + public DrawingContext CreateDrawingContext() { - if(_surface != null) - return new DrawingContext(_surface); - return new DrawingContext(_window.GdkWindow); + var ctx = _surface != null + ? new Media.DrawingContext(_surface) + : new Media.DrawingContext(_window.GdkWindow); + return new DrawingContext(ctx); } public void Dispose() => _surface?.Dispose(); diff --git a/src/Perspex.Application/Application.cs b/src/Perspex.Application/Application.cs index 88f3a23ffe..1e19736154 100644 --- a/src/Perspex.Application/Application.cs +++ b/src/Perspex.Application/Application.cs @@ -177,7 +177,7 @@ namespace Perspex /// The value of Environment.OSVersion.Platform. protected void InitializeSubsystems(int platformID) { - if (platformID == 4 || platformID == 6) + if (true)//platformID == 4 || platformID == 6) { InitializeSubsystem("Perspex.Cairo"); InitializeSubsystem("Perspex.Gtk"); diff --git a/src/Perspex.Controls/Border.cs b/src/Perspex.Controls/Border.cs index 8a70987cb0..bcb1235f25 100644 --- a/src/Perspex.Controls/Border.cs +++ b/src/Perspex.Controls/Border.cs @@ -83,7 +83,7 @@ namespace Perspex.Controls /// Renders the control. /// /// The drawing context. - public override void Render(IDrawingContext context) + public override void Render(DrawingContext context) { var background = Background; var borderBrush = BorderBrush; diff --git a/src/Perspex.Controls/Image.cs b/src/Perspex.Controls/Image.cs index 79b79c6141..0b316689bd 100644 --- a/src/Perspex.Controls/Image.cs +++ b/src/Perspex.Controls/Image.cs @@ -46,7 +46,7 @@ namespace Perspex.Controls /// Renders the control. /// /// The drawing context. - public override void Render(IDrawingContext context) + public override void Render(DrawingContext context) { Bitmap source = Source; diff --git a/src/Perspex.Controls/Panel.cs b/src/Perspex.Controls/Panel.cs index 08e6621adb..bd45bf1e3a 100644 --- a/src/Perspex.Controls/Panel.cs +++ b/src/Perspex.Controls/Panel.cs @@ -172,10 +172,10 @@ namespace Perspex.Controls } /// - /// Renders the visual to a . + /// Renders the visual to a . /// /// The drawing context. - public override void Render(IDrawingContext context) + public override void Render(DrawingContext context) { Brush background = Background; if (background != null) diff --git a/src/Perspex.Controls/Presenters/TextPresenter.cs b/src/Perspex.Controls/Presenters/TextPresenter.cs index 1e6068fd7d..2948cd541a 100644 --- a/src/Perspex.Controls/Presenters/TextPresenter.cs +++ b/src/Perspex.Controls/Presenters/TextPresenter.cs @@ -69,7 +69,7 @@ namespace Perspex.Controls.Presenters return hit.TextPosition + (hit.IsTrailing ? 1 : 0); } - public override void Render(IDrawingContext context) + public override void Render(DrawingContext context) { var selectionStart = SelectionStart; var selectionEnd = SelectionEnd; diff --git a/src/Perspex.Controls/Primitives/AccessText.cs b/src/Perspex.Controls/Primitives/AccessText.cs index 33da32037f..0168974248 100644 --- a/src/Perspex.Controls/Primitives/AccessText.cs +++ b/src/Perspex.Controls/Primitives/AccessText.cs @@ -62,7 +62,7 @@ namespace Perspex.Controls.Primitives /// Renders the to a drawing context. /// /// The drawing context. - public override void Render(IDrawingContext context) + public override void Render(DrawingContext context) { base.Render(context); diff --git a/src/Perspex.Controls/Shapes/Shape.cs b/src/Perspex.Controls/Shapes/Shape.cs index 5f5b989602..58dc24a738 100644 --- a/src/Perspex.Controls/Shapes/Shape.cs +++ b/src/Perspex.Controls/Shapes/Shape.cs @@ -90,7 +90,7 @@ namespace Perspex.Controls.Shapes set { SetValue(StrokeThicknessProperty, value); } } - public override void Render(IDrawingContext context) + public override void Render(DrawingContext context) { var geometry = RenderedGeometry; diff --git a/src/Perspex.Controls/TextBlock.cs b/src/Perspex.Controls/TextBlock.cs index e621369679..b6bec05240 100644 --- a/src/Perspex.Controls/TextBlock.cs +++ b/src/Perspex.Controls/TextBlock.cs @@ -214,7 +214,7 @@ namespace Perspex.Controls /// Renders the to a drawing context. /// /// The drawing context. - public override void Render(IDrawingContext context) + public override void Render(DrawingContext context) { Brush background = Background; diff --git a/src/Perspex.HtmlRenderer/Adapters/GraphicsAdapter.cs b/src/Perspex.HtmlRenderer/Adapters/GraphicsAdapter.cs index 8b52a1b573..6fd918a69b 100644 --- a/src/Perspex.HtmlRenderer/Adapters/GraphicsAdapter.cs +++ b/src/Perspex.HtmlRenderer/Adapters/GraphicsAdapter.cs @@ -32,7 +32,7 @@ namespace TheArtOfDev.HtmlRenderer.Perspex.Adapters /// /// The wrapped Perspex graphics object /// - private readonly IDrawingContext _g; + private readonly DrawingContext _g; /// /// if to release the graphics object on dispose @@ -51,7 +51,7 @@ namespace TheArtOfDev.HtmlRenderer.Perspex.Adapters /// the Perspex graphics object to use /// the initial clip of the graphics /// optional: if to release the graphics object on dispose (default - false) - public GraphicsAdapter(IDrawingContext g, RRect initialClip, bool releaseGraphics = false) + public GraphicsAdapter(DrawingContext g, RRect initialClip, bool releaseGraphics = false) : base(PerspexAdapter.Instance, initialClip) { ArgChecker.AssertArgNotNull(g, "g"); diff --git a/src/Perspex.HtmlRenderer/HtmlContainer.cs b/src/Perspex.HtmlRenderer/HtmlContainer.cs index 9ada42d5ed..e30036b343 100644 --- a/src/Perspex.HtmlRenderer/HtmlContainer.cs +++ b/src/Perspex.HtmlRenderer/HtmlContainer.cs @@ -360,7 +360,7 @@ namespace TheArtOfDev.HtmlRenderer.Perspex /// /// the device to use to render /// the clip rectangle of the html container - public void PerformPaint(IDrawingContext g, Rect clip) + public void PerformPaint(DrawingContext g, Rect clip) { ArgChecker.AssertArgNotNull(g, "g"); diff --git a/src/Perspex.HtmlRenderer/HtmlControl.cs b/src/Perspex.HtmlRenderer/HtmlControl.cs index b720b17bc0..ddd7541923 100644 --- a/src/Perspex.HtmlRenderer/HtmlControl.cs +++ b/src/Perspex.HtmlRenderer/HtmlControl.cs @@ -327,7 +327,7 @@ namespace Perspex.Controls.Html private Size RenderSize => new Size(Bounds.Width, Bounds.Height); - public override void Render(IDrawingContext context) + public override void Render(DrawingContext context) { context.FillRectangle(Background, new Rect(RenderSize)); diff --git a/src/Perspex.SceneGraph/IVisual.cs b/src/Perspex.SceneGraph/IVisual.cs index 7126558483..0491d63fad 100644 --- a/src/Perspex.SceneGraph/IVisual.cs +++ b/src/Perspex.SceneGraph/IVisual.cs @@ -76,10 +76,10 @@ namespace Perspex int ZIndex { get; set; } /// - /// Renders the scene graph node to a . + /// Renders the scene graph node to a . /// /// The context. - void Render(IDrawingContext context); + void Render(DrawingContext context); /// /// Returns a transform that transforms the visual's coordinates into the coordinates diff --git a/src/Perspex.SceneGraph/Media/DrawingContext.cs b/src/Perspex.SceneGraph/Media/DrawingContext.cs new file mode 100644 index 0000000000..5f95ba021b --- /dev/null +++ b/src/Perspex.SceneGraph/Media/DrawingContext.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Perspex.Media.Imaging; + +namespace Perspex.Media +{ + public sealed class DrawingContext : IDisposable + { + private readonly IDrawingContextImpl _impl; + private int _currentLevel; + + public DrawingContext(IDrawingContextImpl impl) + { + _impl = impl; + } + + /// + /// Gets the current transform of the drawing context. + /// + public Matrix CurrentTransform => _impl.Transform; + + /// + /// Draws a bitmap image. + /// + /// The bitmap image. + /// The opacity to draw with. + /// The rect in the image to draw. + /// The rect in the output to draw to. + public void DrawImage(IBitmap source, double opacity, Rect sourceRect, Rect destRect) + => _impl.DrawImage(source, opacity, sourceRect, destRect); + + /// + /// Draws a line. + /// + /// The stroke pen. + /// The first point of the line. + /// The second point of the line. + public void DrawLine(Pen pen, Point p1, Point p2) => _impl.DrawLine(pen, p1, p2); + + /// + /// Draws a geometry. + /// + /// The fill brush. + /// The stroke pen. + /// The geometry. + public void DrawGeometry(Brush brush, Pen pen, Geometry geometry) => _impl.DrawGeometry(brush, pen, geometry); + + /// + /// Draws the outline of a rectangle. + /// + /// The pen. + /// The rectangle bounds. + /// The corner radius. + public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0.0f) + => _impl.DrawRectangle(pen, rect, cornerRadius); + + /// + /// Draws text. + /// + /// The foreground brush. + /// The upper-left corner of the text. + /// The text. + public void DrawText(Brush foreground, Point origin, FormattedText text) + => _impl.DrawText(foreground, origin, text); + + /// + /// Draws a filled rectangle. + /// + /// The brush. + /// The rectangle bounds. + /// The corner radius. + public void FillRectangle(Brush brush, Rect rect, float cornerRadius = 0.0f) + => _impl.FillRectangle(brush, rect, cornerRadius); + + public struct PushedState : IDisposable + { + private readonly int _level; + private readonly DrawingContext _context; + private readonly Matrix _matrix; + private readonly PushedStateType _type; + + public enum PushedStateType + { + None, + Matrix, + Opacity, + Clip + } + + public PushedState(DrawingContext context, PushedStateType type, Matrix matrix = default(Matrix)) + { + _level = context._currentLevel += 1; + _context = context; + _type = type; + _matrix = matrix; + + } + + public void Dispose() + { + if(_type == PushedStateType.None) + return; + if (_context._currentLevel != _level) + throw new InvalidOperationException("Wrong Push/Pop state order"); + _context._currentLevel--; + if (_type == PushedStateType.Matrix) + _context._impl.Transform = _matrix; + else if(_type == PushedStateType.Clip) + _context._impl.PopClip(); + + else if(_type == PushedStateType.Opacity) + _context._impl.PopOpacity(); + } + + + } + + + /// + /// Pushes a clip rectange. + /// + /// The clip rectangle. + /// A disposable used to undo the clip rectangle. + public PushedState PushClip(Rect clip) + { + _impl.PushClip(clip); + return new PushedState(this, PushedState.PushedStateType.Clip); + } + + /// + /// Pushes an opacity value. + /// + /// The opacity. + /// A disposable used to undo the opacity. + public PushedState PushOpacity(double opacity) + //TODO: Elimintate platform-specific push opacity call + { + _impl.PushOpacity(opacity); + return new PushedState(this, PushedState.PushedStateType.Opacity); + } + + /// + /// Pushes a matrix transformation. + /// + /// The matrix + /// A disposable used to undo the transformation. + public PushedState PushTransform(Matrix matrix) + { + var oldMatrix = CurrentTransform; + matrix = oldMatrix*matrix; + _impl.Transform = matrix; + return new PushedState(this, PushedState.PushedStateType.Matrix, oldMatrix); + } + + public void Dispose() => _impl.Dispose(); + } +} diff --git a/src/Perspex.SceneGraph/Media/IDrawingContext.cs b/src/Perspex.SceneGraph/Media/IDrawingContext.cs index 92d1fc4e98..4d144b33e0 100644 --- a/src/Perspex.SceneGraph/Media/IDrawingContext.cs +++ b/src/Perspex.SceneGraph/Media/IDrawingContext.cs @@ -9,12 +9,12 @@ namespace Perspex.Media /// /// Defines the interface through which drawing occurs. /// - public interface IDrawingContext : IDisposable + public interface IDrawingContextImpl : IDisposable { /// - /// Gets the current transform of the drawing context. + /// Gets or sets the current transform of the drawing context. /// - Matrix CurrentTransform { get; } + Matrix Transform { get; set; } /// /// Draws a bitmap image. @@ -70,20 +70,17 @@ namespace Perspex.Media /// /// The clip rectangle. /// A disposable used to undo the clip rectangle. - IDisposable PushClip(Rect clip); + void PushClip(Rect clip); + + void PopClip(); /// /// Pushes an opacity value. /// /// The opacity. /// A disposable used to undo the opacity. - IDisposable PushOpacity(double opacity); + void PushOpacity(double opacity); - /// - /// Pushes a matrix transformation. - /// - /// The matrix - /// A disposable used to undo the transformation. - IDisposable PushTransform(Matrix matrix); + void PopOpacity(); } } diff --git a/src/Perspex.SceneGraph/Media/Imaging/RenderTargetBitmap.cs b/src/Perspex.SceneGraph/Media/Imaging/RenderTargetBitmap.cs index 5b8bbea6da..20fc55ebd8 100644 --- a/src/Perspex.SceneGraph/Media/Imaging/RenderTargetBitmap.cs +++ b/src/Perspex.SceneGraph/Media/Imaging/RenderTargetBitmap.cs @@ -47,7 +47,7 @@ namespace Perspex.Media.Imaging return factory.CreateRenderTargetBitmap(width, height); } - public IDrawingContext CreateDrawingContext() => PlatformImpl.CreateDrawingContext(); + public DrawingContext CreateDrawingContext() => PlatformImpl.CreateDrawingContext(); void IRenderTarget.Resize(int width, int height) { diff --git a/src/Perspex.SceneGraph/Media/ValidatingDrawingContext.cs b/src/Perspex.SceneGraph/Media/ValidatingDrawingContext.cs deleted file mode 100644 index 86ae342894..0000000000 --- a/src/Perspex.SceneGraph/Media/ValidatingDrawingContext.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Reactive.Disposables; -using Perspex.Media.Imaging; - -namespace Perspex.Media -{ - public class ValidatingDrawingContext : IDrawingContext - { - private readonly IDrawingContext _base; - - public ValidatingDrawingContext(IDrawingContext @base) - { - _base = @base; - } - - public void Dispose() - { - _base.Dispose(); - } - - public Matrix CurrentTransform => _base.CurrentTransform; - public void DrawImage(IBitmap source, double opacity, Rect sourceRect, Rect destRect) - { - _base.DrawImage(source, opacity, sourceRect, destRect); - } - - public void DrawLine(Pen pen, Point p1, Point p2) - { - _base.DrawLine(pen, p1, p2); - } - - public void DrawGeometry(Brush brush, Pen pen, Geometry geometry) - { - _base.DrawGeometry(brush, pen, geometry); - } - - public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0) - { - _base.DrawRectangle(pen, rect, cornerRadius); - } - - public void DrawText(Brush foreground, Point origin, FormattedText text) - { - _base.DrawText(foreground, origin, text); - } - - public void FillRectangle(Brush brush, Rect rect, float cornerRadius = 0) - { - _base.FillRectangle(brush, rect, cornerRadius); - } - - - Stack _stateStack = new Stack(); - - IDisposable Transform(IDisposable disposable) - { - _stateStack.Push(disposable); - return Disposable.Create(() => - { - var current = _stateStack.Peek(); - if (current != disposable) - throw new InvalidOperationException("Invalid push/pop order"); - current.Dispose(); - _stateStack.Pop(); - }); - } - - public IDisposable PushClip(Rect clip) => Transform(_base.PushClip(clip)); - - public IDisposable PushOpacity(double opacity) => Transform(_base.PushOpacity(opacity)); - - public IDisposable PushTransform(Matrix matrix) => Transform(_base.PushTransform(matrix)); - } -} \ No newline at end of file diff --git a/src/Perspex.SceneGraph/Perspex.SceneGraph.csproj b/src/Perspex.SceneGraph/Perspex.SceneGraph.csproj index e09bed7d90..d2d897142f 100644 --- a/src/Perspex.SceneGraph/Perspex.SceneGraph.csproj +++ b/src/Perspex.SceneGraph/Perspex.SceneGraph.csproj @@ -64,6 +64,7 @@ + @@ -99,7 +100,6 @@ - diff --git a/src/Perspex.SceneGraph/Platform/IRenderTarget.cs b/src/Perspex.SceneGraph/Platform/IRenderTarget.cs index fdbf391a05..a5c8f71d79 100644 --- a/src/Perspex.SceneGraph/Platform/IRenderTarget.cs +++ b/src/Perspex.SceneGraph/Platform/IRenderTarget.cs @@ -15,9 +15,9 @@ namespace Perspex.Platform public interface IRenderTarget : IDisposable { /// - /// Creates an for a rendering session. + /// Creates an for a rendering session. /// - IDrawingContext CreateDrawingContext(); + DrawingContext CreateDrawingContext(); /// /// Resizes the rendered viewport. diff --git a/src/Perspex.SceneGraph/Rendering/RendererBase.cs b/src/Perspex.SceneGraph/Rendering/RendererBase.cs index ba4a0d35c1..679407f7bf 100644 --- a/src/Perspex.SceneGraph/Rendering/RendererBase.cs +++ b/src/Perspex.SceneGraph/Rendering/RendererBase.cs @@ -35,7 +35,7 @@ namespace Perspex.Rendering /// The visual to render. /// /// The drawing context. - public static void Render(this IDrawingContext context, IVisual visual) + public static void Render(this DrawingContext context, IVisual visual) { var opacity = visual.Opacity; if (visual.IsVisible && opacity > 0) @@ -54,7 +54,7 @@ namespace Perspex.Rendering using (context.PushTransform(m)) using (context.PushOpacity(opacity)) - using (visual.ClipToBounds ? context.PushClip(new Rect(visual.Bounds.Size)) : null) + using (visual.ClipToBounds ? context.PushClip(new Rect(visual.Bounds.Size)) : default(DrawingContext.PushedState)) { visual.Render(context); foreach (var child in visual.VisualChildren.OrderBy(x => x.ZIndex)) diff --git a/src/Perspex.SceneGraph/Visual.cs b/src/Perspex.SceneGraph/Visual.cs index e835c6a76c..2bbc290685 100644 --- a/src/Perspex.SceneGraph/Visual.cs +++ b/src/Perspex.SceneGraph/Visual.cs @@ -229,10 +229,10 @@ namespace Perspex } /// - /// Renders the visual to a . + /// Renders the visual to a . /// /// The drawing context. - public virtual void Render(IDrawingContext context) + public virtual void Render(DrawingContext context) { Contract.Requires(context != null); } diff --git a/src/Windows/Perspex.Direct2D1/Media/DrawingContext.cs b/src/Windows/Perspex.Direct2D1/Media/DrawingContext.cs index 0b6abe9353..b10208d7e0 100644 --- a/src/Windows/Perspex.Direct2D1/Media/DrawingContext.cs +++ b/src/Windows/Perspex.Direct2D1/Media/DrawingContext.cs @@ -2,6 +2,8 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections; +using System.Collections.Generic; using System.Reactive.Disposables; using Perspex.Media; using SharpDX; @@ -13,7 +15,7 @@ namespace Perspex.Direct2D1.Media /// /// Draws using Direct2D1. /// - public class DrawingContext : IDrawingContext, IDisposable + public class DrawingContext : IDrawingContextImpl, IDisposable { /// /// The Direct2D1 render target. @@ -42,10 +44,10 @@ namespace Perspex.Direct2D1.Media /// /// Gets the current transform of the drawing context. /// - public Matrix CurrentTransform + public Matrix Transform { get { return _renderTarget.Transform.ToPerspex(); } - private set { _renderTarget.Transform = value.ToDirect2D(); } + set { _renderTarget.Transform = value.ToDirect2D(); } } /// @@ -53,6 +55,8 @@ namespace Perspex.Direct2D1.Media /// public void Dispose() { + foreach (var layer in _layerPool) + layer.Dispose(); _renderTarget.EndDraw(); } @@ -233,22 +237,24 @@ namespace Perspex.Direct2D1.Media /// /// The clip rectangle. /// A disposable used to undo the clip rectangle. - public IDisposable PushClip(Rect clip) + public void PushClip(Rect clip) { _renderTarget.PushAxisAlignedClip(clip.ToSharpDX(), AntialiasMode.PerPrimitive); + } - return Disposable.Create(() => - { - _renderTarget.PopAxisAlignedClip(); - }); + public void PopClip() + { + _renderTarget.PopAxisAlignedClip(); } + Stack _layers = new Stack(); + private readonly Stack _layerPool = new Stack(); /// /// Pushes an opacity value. /// /// The opacity. /// A disposable used to undo the opacity. - public IDisposable PushOpacity(double opacity) + public void PushOpacity(double opacity) { if (opacity < 1) { @@ -256,41 +262,26 @@ namespace Perspex.Direct2D1.Media { ContentBounds = RectangleF.Infinite, MaskTransform = Matrix3x2.Identity, - Opacity = (float)opacity, + Opacity = (float) opacity, }; - var layer = new Layer(_renderTarget); - + var layer = _layerPool.Count != 0 ? _layerPool.Pop() : new Layer(_renderTarget); _renderTarget.PushLayer(ref parameters, layer); - return Disposable.Create(() => - { - _renderTarget.PopLayer(); - layer.Dispose(); - }); + _layers.Push(layer); } else - { - return Disposable.Empty; - } + _layers.Push(null); } - /// - /// Pushes a matrix transformation. - /// - /// The matrix - /// A disposable used to undo the transformation. - public IDisposable PushTransform(Matrix matrix) + public void PopOpacity() { - Matrix3x2 m3x2 = matrix.ToDirect2D(); - Matrix3x2 transform = _renderTarget.Transform * m3x2; - _renderTarget.Transform = transform; - - return Disposable.Create(() => + var layer = _layers.Pop(); + if (layer != null) { - m3x2.Invert(); - _renderTarget.Transform = transform * m3x2; - }); + _renderTarget.PopLayer(); + _layerPool.Push(layer); + } } /// diff --git a/src/Windows/Perspex.Direct2D1/Media/Imaging/RenderTargetBitmapImpl.cs b/src/Windows/Perspex.Direct2D1/Media/Imaging/RenderTargetBitmapImpl.cs index 97efa00d47..cf785aa444 100644 --- a/src/Windows/Perspex.Direct2D1/Media/Imaging/RenderTargetBitmapImpl.cs +++ b/src/Windows/Perspex.Direct2D1/Media/Imaging/RenderTargetBitmapImpl.cs @@ -38,7 +38,7 @@ namespace Perspex.Direct2D1.Media // TODO: } - public IDrawingContext CreateDrawingContext() => new RenderTarget(_target).CreateDrawingContext(); + public Perspex.Media.DrawingContext CreateDrawingContext() => new RenderTarget(_target).CreateDrawingContext(); void IRenderTarget.Resize(int width, int height) { diff --git a/src/Windows/Perspex.Direct2D1/RenderTarget.cs b/src/Windows/Perspex.Direct2D1/RenderTarget.cs index 5feabdde87..66ffe80448 100644 --- a/src/Windows/Perspex.Direct2D1/RenderTarget.cs +++ b/src/Windows/Perspex.Direct2D1/RenderTarget.cs @@ -8,6 +8,7 @@ using Perspex.Platform; using Perspex.Rendering; using SharpDX; using SharpDX.Direct2D1; +using DrawingContext = Perspex.Media.DrawingContext; using DwFactory = SharpDX.DirectWrite.Factory; namespace Perspex.Direct2D1 @@ -91,24 +92,12 @@ namespace Perspex.Direct2D1 window.Resize(new Size2(width, height)); } - IDrawingContext Wrap(IDrawingContext ctx) - { -#if DEBUG - return new ValidatingDrawingContext(ctx); -#endif -#pragma warning disable 162 - return ctx; -#pragma warning restore 162 - } - /// /// Creates a drawing context for a rendering session. /// - /// An . - public IDrawingContext CreateDrawingContext() - { - return Wrap(new DrawingContext(_renderTarget, DirectWriteFactory)); - } + /// An . + public DrawingContext CreateDrawingContext() + => new DrawingContext(new Media.DrawingContext(_renderTarget, DirectWriteFactory)); public void Dispose() { From 50f0865a646ded52547f8cf0ad9fe55e96d3a7aa Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 3 Oct 2015 10:37:04 +0300 Subject: [PATCH 58/72] Replaced PushTransform by PushPreTransform and PushPostTransform --- src/Gtk/Perspex.Cairo/Media/TileBrushes.cs | 2 +- src/Perspex.Application/Application.cs | 2 +- src/Perspex.SceneGraph/Media/DrawingContext.cs | 8 ++++++-- src/Perspex.SceneGraph/Rendering/RendererBase.cs | 5 +++-- src/Windows/Perspex.Direct2D1/Media/VisualBrushImpl.cs | 2 +- 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Gtk/Perspex.Cairo/Media/TileBrushes.cs b/src/Gtk/Perspex.Cairo/Media/TileBrushes.cs index 94fb19ea0b..98361427f0 100644 --- a/src/Gtk/Perspex.Cairo/Media/TileBrushes.cs +++ b/src/Gtk/Perspex.Cairo/Media/TileBrushes.cs @@ -113,7 +113,7 @@ namespace Perspex.Cairo.Media out drawRect); using (ctx.PushClip(drawRect)) - using (ctx.PushTransform(transform)) + using (ctx.PushPostTransform(transform)) { ctx.Render(visual); } diff --git a/src/Perspex.Application/Application.cs b/src/Perspex.Application/Application.cs index 1e19736154..88f3a23ffe 100644 --- a/src/Perspex.Application/Application.cs +++ b/src/Perspex.Application/Application.cs @@ -177,7 +177,7 @@ namespace Perspex /// The value of Environment.OSVersion.Platform. protected void InitializeSubsystems(int platformID) { - if (true)//platformID == 4 || platformID == 6) + if (platformID == 4 || platformID == 6) { InitializeSubsystem("Perspex.Cairo"); InitializeSubsystem("Perspex.Gtk"); diff --git a/src/Perspex.SceneGraph/Media/DrawingContext.cs b/src/Perspex.SceneGraph/Media/DrawingContext.cs index 5f95ba021b..32edc8d8fa 100644 --- a/src/Perspex.SceneGraph/Media/DrawingContext.cs +++ b/src/Perspex.SceneGraph/Media/DrawingContext.cs @@ -147,10 +147,14 @@ namespace Perspex.Media /// /// The matrix /// A disposable used to undo the transformation. - public PushedState PushTransform(Matrix matrix) + public PushedState PushPostTransform(Matrix matrix) => PushSetTransform(CurrentTransform*matrix); + + public PushedState PushPreTransform(Matrix matrix) => PushSetTransform(matrix*CurrentTransform); + + + PushedState PushSetTransform(Matrix matrix) { var oldMatrix = CurrentTransform; - matrix = oldMatrix*matrix; _impl.Transform = matrix; return new PushedState(this, PushedState.PushedStateType.Matrix, oldMatrix); } diff --git a/src/Perspex.SceneGraph/Rendering/RendererBase.cs b/src/Perspex.SceneGraph/Rendering/RendererBase.cs index 679407f7bf..88601e7ef8 100644 --- a/src/Perspex.SceneGraph/Rendering/RendererBase.cs +++ b/src/Perspex.SceneGraph/Rendering/RendererBase.cs @@ -50,9 +50,10 @@ namespace Perspex.Rendering var offset = Matrix.CreateTranslation(origin); renderTransform = (-offset)*visual.RenderTransform.Value*(offset); } - m = context.CurrentTransform.Invert()*renderTransform*m*context.CurrentTransform; + m = renderTransform*m; - using (context.PushTransform(m)) + + using (context.PushPreTransform(m)) using (context.PushOpacity(opacity)) using (visual.ClipToBounds ? context.PushClip(new Rect(visual.Bounds.Size)) : default(DrawingContext.PushedState)) { diff --git a/src/Windows/Perspex.Direct2D1/Media/VisualBrushImpl.cs b/src/Windows/Perspex.Direct2D1/Media/VisualBrushImpl.cs index ddb82fba19..74377b67e6 100644 --- a/src/Windows/Perspex.Direct2D1/Media/VisualBrushImpl.cs +++ b/src/Windows/Perspex.Direct2D1/Media/VisualBrushImpl.cs @@ -55,7 +55,7 @@ namespace Perspex.Direct2D1.Media using (var ctx = renderer.CreateDrawingContext()) using (ctx.PushClip(drawRect)) - using (ctx.PushTransform(transform)) + using (ctx.PushPostTransform(transform)) { ctx.Render(visual); } From 8a2578a98576d622d864b6852a36b63108b2d77d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 3 Oct 2015 11:06:26 +0300 Subject: [PATCH 59/72] Implemented transform containers --- .../Media/DrawingContext.cs | 56 ++++++++++++++++--- .../Rendering/RendererBase.cs | 4 +- 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/src/Perspex.SceneGraph/Media/DrawingContext.cs b/src/Perspex.SceneGraph/Media/DrawingContext.cs index 32edc8d8fa..a191cb1fce 100644 --- a/src/Perspex.SceneGraph/Media/DrawingContext.cs +++ b/src/Perspex.SceneGraph/Media/DrawingContext.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -12,15 +13,42 @@ namespace Perspex.Media private readonly IDrawingContextImpl _impl; private int _currentLevel; + private Stack _transformContainers = new Stack(); + + struct TransformContainer + { + public Matrix LocalTransform; + public Matrix ContainerTransform; + + public TransformContainer(Matrix localTransform, Matrix containerTransform) + { + LocalTransform = localTransform; + ContainerTransform = containerTransform; + } + } + public DrawingContext(IDrawingContextImpl impl) { _impl = impl; } + + private Matrix _currentTransform = Matrix.Identity; + /// /// Gets the current transform of the drawing context. /// - public Matrix CurrentTransform => _impl.Transform; + public Matrix CurrentTransform + { + get { return _currentTransform; } + private set + { + _currentTransform = value; + _impl.Transform = _currentTransform*_currentContainerTransform; + } + } + + private Matrix _currentContainerTransform = Matrix.Identity; /// /// Draws a bitmap image. @@ -87,7 +115,8 @@ namespace Perspex.Media None, Matrix, Opacity, - Clip + Clip, + MatrixContainer } public PushedState(DrawingContext context, PushedStateType type, Matrix matrix = default(Matrix)) @@ -107,15 +136,18 @@ namespace Perspex.Media throw new InvalidOperationException("Wrong Push/Pop state order"); _context._currentLevel--; if (_type == PushedStateType.Matrix) - _context._impl.Transform = _matrix; + _context.CurrentTransform = _matrix; else if(_type == PushedStateType.Clip) _context._impl.PopClip(); - else if(_type == PushedStateType.Opacity) _context._impl.PopOpacity(); + else if (_type == PushedStateType.MatrixContainer) + { + var cont = _context._transformContainers.Pop(); + _context._currentContainerTransform = cont.ContainerTransform; + _context.CurrentTransform = cont.LocalTransform; + } } - - } @@ -155,10 +187,20 @@ namespace Perspex.Media PushedState PushSetTransform(Matrix matrix) { var oldMatrix = CurrentTransform; - _impl.Transform = matrix; + CurrentTransform = matrix; + return new PushedState(this, PushedState.PushedStateType.Matrix, oldMatrix); } + + public PushedState PushTransformContainer() + { + _transformContainers.Push(new TransformContainer(CurrentTransform, _currentContainerTransform)); + _currentContainerTransform = CurrentTransform*_currentContainerTransform; + _currentTransform = Matrix.Identity; + return new PushedState(this, PushedState.PushedStateType.MatrixContainer); + } + public void Dispose() => _impl.Dispose(); } } diff --git a/src/Perspex.SceneGraph/Rendering/RendererBase.cs b/src/Perspex.SceneGraph/Rendering/RendererBase.cs index 88601e7ef8..7a7db8f842 100644 --- a/src/Perspex.SceneGraph/Rendering/RendererBase.cs +++ b/src/Perspex.SceneGraph/Rendering/RendererBase.cs @@ -52,10 +52,10 @@ namespace Perspex.Rendering } m = renderTransform*m; - - using (context.PushPreTransform(m)) + using (context.PushPostTransform(m)) using (context.PushOpacity(opacity)) using (visual.ClipToBounds ? context.PushClip(new Rect(visual.Bounds.Size)) : default(DrawingContext.PushedState)) + using (context.PushTransformContainer()) { visual.Render(context); foreach (var child in visual.VisualChildren.OrderBy(x => x.ZIndex)) From ac5485d24935ba3efd53c2a783075568eb496312 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 3 Oct 2015 11:25:32 +0300 Subject: [PATCH 60/72] Fixed cairo unit tests, however that change doesn't have any sense --- src/Gtk/Perspex.Cairo/Media/DrawingContext.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Gtk/Perspex.Cairo/Media/DrawingContext.cs b/src/Gtk/Perspex.Cairo/Media/DrawingContext.cs index 3efb60e5c5..f1255b9ce1 100644 --- a/src/Gtk/Perspex.Cairo/Media/DrawingContext.cs +++ b/src/Gtk/Perspex.Cairo/Media/DrawingContext.cs @@ -143,7 +143,6 @@ namespace Perspex.Cairo.Media Transform *= impl.Transform; - if (brush != null) { _context.AppendPath(impl.Path); @@ -155,6 +154,8 @@ namespace Perspex.Cairo.Media _context.Fill(); } } + Transform = oldMatrix; + if (pen != null) { _context.AppendPath(impl.Path); @@ -163,7 +164,6 @@ namespace Perspex.Cairo.Media _context.Stroke(); } } - Transform = oldMatrix; } /// From 43a66901fc0beb3c691d886fecdc2b3bdd2ed821 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 3 Oct 2015 11:02:50 +0200 Subject: [PATCH 61/72] Remove ExpressionValue. Use PerspexProperty.UnsetValue instead. --- .../Binding/XamlBinding.cs | 5 +++ .../Templates/TreeDataTemplate.cs | 2 +- .../Perspex.Markup/Binding/ExpressionNode.cs | 20 +++++----- .../Binding/ExpressionObserver.cs | 4 +- .../Binding/ExpressionSubject.cs | 2 +- .../Perspex.Markup/Binding/ExpressionValue.cs | 38 ------------------- .../Perspex.Markup/Binding/IndexerNode.cs | 8 ++-- .../Perspex.Markup/Binding/LogicalNotNode.cs | 12 +++--- .../Binding/PropertyAccessorNode.cs | 14 +++---- .../Perspex.Markup/Perspex.Markup.csproj | 1 - src/Perspex.Base/IPropertyBag.cs | 5 +++ src/Perspex.Base/PerspexObject.cs | 5 +++ .../ExpressionObserverTests_Indexer.cs | 23 +++++------ .../ExpressionObserverTests_Negation.cs | 19 ++++------ .../ExpressionObserverTests_Observable.cs | 8 ++-- .../ExpressionObserverTests_Property.cs | 31 +++++++-------- .../Binding/ExpressionObserverTests_Task.cs | 10 ++--- .../SelectorTests_Child.cs | 8 ++++ .../SelectorTests_Descendent.cs | 8 ++++ .../TestControlBase.cs | 8 ++++ .../TestTemplatedControl.cs | 8 ++++ 21 files changed, 118 insertions(+), 121 deletions(-) delete mode 100644 src/Markup/Perspex.Markup/Binding/ExpressionValue.cs diff --git a/src/Markup/Perspex.Markup.Xaml/Binding/XamlBinding.cs b/src/Markup/Perspex.Markup.Xaml/Binding/XamlBinding.cs index 3bf709e928..2bf9b82bcf 100644 --- a/src/Markup/Perspex.Markup.Xaml/Binding/XamlBinding.cs +++ b/src/Markup/Perspex.Markup.Xaml/Binding/XamlBinding.cs @@ -29,6 +29,11 @@ namespace Perspex.Markup.Xaml.Binding public void Bind(IObservablePropertyBag instance, PerspexProperty property) { + if (property == Control.DataContextProperty && instance.InheritanceParent != null) + { + instance = instance.InheritanceParent as IObservablePropertyBag ?? instance; + } + var subject = new ExpressionSubject(CreateExpressionObserver(instance)); Bind(instance, property, subject); } diff --git a/src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs index 0868dae69f..15cdf822bb 100644 --- a/src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs +++ b/src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs @@ -32,7 +32,7 @@ namespace Perspex.Markup.Xaml.Templates if (ItemsSource != null) { var obs = new ExpressionObserver(item, ItemsSource.SourcePropertyPath); - return obs.Take(1).Wait().Value as IEnumerable; + return obs.Take(1).Wait() as IEnumerable; } return null; diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs b/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs index e5dc9f2810..d3520818dc 100644 --- a/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs +++ b/src/Markup/Perspex.Markup/Binding/ExpressionNode.cs @@ -6,13 +6,13 @@ using System.Reactive.Subjects; namespace Perspex.Markup.Binding { - internal abstract class ExpressionNode : IObservable + internal abstract class ExpressionNode : IObservable { private object _target; - private Subject _subject; + private Subject _subject; - private ExpressionValue _value = ExpressionValue.None; + private object _value = PerspexProperty.UnsetValue; public ExpressionNode Next { get; set; } @@ -34,17 +34,17 @@ namespace Perspex.Markup.Binding } else { - CurrentValue = ExpressionValue.None; + CurrentValue = PerspexProperty.UnsetValue; } if (Next != null) { - Next.Target = CurrentValue.Value; + Next.Target = CurrentValue; } } } - public ExpressionValue CurrentValue + public object CurrentValue { get { @@ -57,7 +57,7 @@ namespace Perspex.Markup.Binding if (Next != null) { - Next.Target = value.Value; + Next.Target = value; } if (_subject != null) @@ -72,7 +72,7 @@ namespace Perspex.Markup.Binding return Next?.SetValue(value) ?? false; } - public virtual IDisposable Subscribe(IObserver observer) + public virtual IDisposable Subscribe(IObserver observer) { if (Next != null) { @@ -82,7 +82,7 @@ namespace Perspex.Markup.Binding { if (_subject == null) { - _subject = new Subject(); + _subject = new Subject(); } observer.OnNext(CurrentValue); @@ -92,7 +92,7 @@ namespace Perspex.Markup.Binding protected virtual void SubscribeAndUpdate(object target) { - CurrentValue = new ExpressionValue(target); + CurrentValue = target; } protected virtual void Unsubscribe(object target) diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs b/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs index ff98e82aeb..2e76d58e24 100644 --- a/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs +++ b/src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs @@ -10,7 +10,7 @@ namespace Perspex.Markup.Binding /// /// Observes and sets the value of an expression on an object. /// - public class ExpressionObserver : ObservableBase, IDescription + public class ExpressionObserver : ObservableBase, IDescription { private object _root; private int _count; @@ -83,7 +83,7 @@ namespace Perspex.Markup.Binding string IDescription.Description => Expression; /// - protected override IDisposable SubscribeCore(IObserver observer) + protected override IDisposable SubscribeCore(IObserver observer) { IncrementCount(); diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionSubject.cs b/src/Markup/Perspex.Markup/Binding/ExpressionSubject.cs index ac5700e512..b8dd316b36 100644 --- a/src/Markup/Perspex.Markup/Binding/ExpressionSubject.cs +++ b/src/Markup/Perspex.Markup/Binding/ExpressionSubject.cs @@ -45,7 +45,7 @@ namespace Perspex.Markup.Binding /// public IDisposable Subscribe(IObserver observer) { - return _inner.Select(x => x.Value).Subscribe(observer); + return _inner.Subscribe(observer); } } } diff --git a/src/Markup/Perspex.Markup/Binding/ExpressionValue.cs b/src/Markup/Perspex.Markup/Binding/ExpressionValue.cs deleted file mode 100644 index 2441acda73..0000000000 --- a/src/Markup/Perspex.Markup/Binding/ExpressionValue.cs +++ /dev/null @@ -1,38 +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; - -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 index 953852a6db..f29dcce741 100644 --- a/src/Markup/Perspex.Markup/Binding/IndexerNode.cs +++ b/src/Markup/Perspex.Markup/Binding/IndexerNode.cs @@ -83,24 +83,24 @@ namespace Perspex.Markup.Binding } } - private ExpressionValue GetValue(object target) + private object 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)); + return ((Array)target).GetValue(_intArgs); } else if (target is IList && _intArgs?.Length == 1) { if (_intArgs[0] < list.Count) { - return new ExpressionValue(list[_intArgs[0]]); + return list[_intArgs[0]]; } } - return ExpressionValue.None; + return PerspexProperty.UnsetValue; } } } diff --git a/src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs b/src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs index d20972c639..81ce06c301 100644 --- a/src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs +++ b/src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs @@ -14,19 +14,19 @@ namespace Perspex.Markup.Binding throw new NotSupportedException("Cannot set a negated binding."); } - public override IDisposable Subscribe(IObserver observer) + public override IDisposable Subscribe(IObserver observer) { return Next.Select(x => Negate(x)).Subscribe(observer); } - private ExpressionValue Negate(ExpressionValue v) + private object Negate(object v) { - if (v.HasValue) + if (v != PerspexProperty.UnsetValue) { try { - var boolean = Convert.ToBoolean(v.Value, CultureInfo.InvariantCulture); - return new ExpressionValue(!boolean); + var boolean = Convert.ToBoolean(v, CultureInfo.InvariantCulture); + return !boolean; } catch { @@ -34,7 +34,7 @@ namespace Perspex.Markup.Binding } } - return ExpressionValue.None; + return PerspexProperty.UnsetValue; } } } diff --git a/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs b/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs index af2db5d63a..1015bcfa0a 100644 --- a/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs +++ b/src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs @@ -69,7 +69,7 @@ namespace Perspex.Markup.Binding if (!set) { - CurrentValue = ExpressionValue.None; + CurrentValue = PerspexProperty.UnsetValue; } } @@ -115,11 +115,11 @@ namespace Perspex.Markup.Binding // ReactiveCommand is an IObservable but we want to bind to it, not its value. if (observable != null && command == null) { - CurrentValue = ExpressionValue.None; + CurrentValue = PerspexProperty.UnsetValue; set = true; _subscription = observable .ObserveOn(SynchronizationContext.Current) - .Subscribe(x => CurrentValue = new ExpressionValue(x)); + .Subscribe(x => CurrentValue = x); } else if (task != null) { @@ -129,13 +129,13 @@ namespace Perspex.Markup.Binding { if (task.Status == TaskStatus.RanToCompletion) { - CurrentValue = new ExpressionValue(resultProperty.GetValue(task)); + CurrentValue = resultProperty.GetValue(task); set = true; } else { task.ContinueWith( - x => CurrentValue = new ExpressionValue(resultProperty.GetValue(task)), + x => CurrentValue = resultProperty.GetValue(task), TaskScheduler.FromCurrentSynchronizationContext()) .ConfigureAwait(false); } @@ -143,13 +143,13 @@ namespace Perspex.Markup.Binding } else { - CurrentValue = new ExpressionValue(value); + CurrentValue = value; set = true; } if (!set) { - CurrentValue = ExpressionValue.None; + CurrentValue = PerspexProperty.UnsetValue; } } diff --git a/src/Markup/Perspex.Markup/Perspex.Markup.csproj b/src/Markup/Perspex.Markup/Perspex.Markup.csproj index 32fb2e3233..1f2da5171e 100644 --- a/src/Markup/Perspex.Markup/Perspex.Markup.csproj +++ b/src/Markup/Perspex.Markup/Perspex.Markup.csproj @@ -38,7 +38,6 @@ - diff --git a/src/Perspex.Base/IPropertyBag.cs b/src/Perspex.Base/IPropertyBag.cs index 68b1b6fed8..fe29c349ef 100644 --- a/src/Perspex.Base/IPropertyBag.cs +++ b/src/Perspex.Base/IPropertyBag.cs @@ -8,6 +8,11 @@ namespace Perspex /// public interface IPropertyBag { + /// + /// Gets the object that inherited values are inherited from. + /// + IPropertyBag InheritanceParent { get; } + /// /// Clears a 's local value. /// diff --git a/src/Perspex.Base/PerspexObject.cs b/src/Perspex.Base/PerspexObject.cs index a08011d025..03d5fecd69 100644 --- a/src/Perspex.Base/PerspexObject.cs +++ b/src/Perspex.Base/PerspexObject.cs @@ -100,6 +100,11 @@ namespace Perspex remove { _inpcChanged -= value; } } + /// + /// Gets the object that inherited values are inherited from. + /// + IPropertyBag IPropertyBag.InheritanceParent => InheritanceParent; + /// /// Gets or sets the parent object that inherited values /// are inherited from. diff --git a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Indexer.cs b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Indexer.cs index f544652c52..36bc2afbde 100644 --- a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Indexer.cs +++ b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Indexer.cs @@ -19,8 +19,7 @@ namespace Perspex.Markup.UnitTests.Binding var target = new ExpressionObserver(data, "Foo[1]"); var result = await target.Take(1); - Assert.True(result.HasValue); - Assert.Equal("bar", result.Value); + Assert.Equal("bar", result); } [Fact] @@ -30,8 +29,7 @@ namespace Perspex.Markup.UnitTests.Binding var target = new ExpressionObserver(data, "Foo[1, 1]"); var result = await target.Take(1); - Assert.True(result.HasValue); - Assert.Equal("qux", result.Value); + Assert.Equal("qux", result); } [Fact] @@ -41,8 +39,7 @@ namespace Perspex.Markup.UnitTests.Binding var target = new ExpressionObserver(data, "Foo[1]"); var result = await target.Take(1); - Assert.True(result.HasValue); - Assert.Equal("bar", result.Value); + Assert.Equal("bar", result); } [Fact] @@ -52,10 +49,10 @@ namespace Perspex.Markup.UnitTests.Binding var target = new ExpressionObserver(data, "Foo[2]"); var result = new List(); - var sub = target.Subscribe(x => result.Add(x.Value)); + var sub = target.Subscribe(x => result.Add(x)); data.Foo.Add("baz"); - Assert.Equal(new[] { null, "baz" }, result); + Assert.Equal(new[] { PerspexProperty.UnsetValue, "baz" }, result); } [Fact] @@ -65,7 +62,7 @@ namespace Perspex.Markup.UnitTests.Binding var target = new ExpressionObserver(data, "Foo[0]"); var result = new List(); - var sub = target.Subscribe(x => result.Add(x.Value)); + var sub = target.Subscribe(x => result.Add(x)); data.Foo.RemoveAt(0); Assert.Equal(new[] { "foo", "bar" }, result); @@ -78,7 +75,7 @@ namespace Perspex.Markup.UnitTests.Binding var target = new ExpressionObserver(data, "Foo[1]"); var result = new List(); - var sub = target.Subscribe(x => result.Add(x.Value)); + var sub = target.Subscribe(x => result.Add(x)); data.Foo[1] = "baz"; Assert.Equal(new[] { "bar", "baz" }, result); @@ -91,7 +88,7 @@ namespace Perspex.Markup.UnitTests.Binding var target = new ExpressionObserver(data, "Foo[1]"); var result = new List(); - var sub = target.Subscribe(x => result.Add(x.Value)); + var sub = target.Subscribe(x => result.Add(x)); data.Foo.Move(0, 1); Assert.Equal(new[] { "bar", "foo" }, result); @@ -104,10 +101,10 @@ namespace Perspex.Markup.UnitTests.Binding var target = new ExpressionObserver(data, "Foo[1]"); var result = new List(); - var sub = target.Subscribe(x => result.Add(x.Value)); + var sub = target.Subscribe(x => result.Add(x)); data.Foo.Clear(); - Assert.Equal(new[] { "bar", null }, result); + Assert.Equal(new[] { "bar", PerspexProperty.UnsetValue }, result); } } } diff --git a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Negation.cs b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Negation.cs index 614f27b34c..2019b884b0 100644 --- a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Negation.cs +++ b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Negation.cs @@ -17,8 +17,7 @@ namespace Perspex.Markup.UnitTests.Binding var target = new ExpressionObserver(data, "!Foo"); var result = await target.Take(1); - Assert.True(result.HasValue); - Assert.Equal(false, result.Value); + Assert.Equal(false, result); } [Fact] @@ -28,8 +27,7 @@ namespace Perspex.Markup.UnitTests.Binding var target = new ExpressionObserver(data, "!Foo"); var result = await target.Take(1); - Assert.True(result.HasValue); - Assert.Equal(true, result.Value); + Assert.Equal(true, result); } [Fact] @@ -39,8 +37,7 @@ namespace Perspex.Markup.UnitTests.Binding var target = new ExpressionObserver(data, "!Foo"); var result = await target.Take(1); - Assert.True(result.HasValue); - Assert.Equal(false, result.Value); + Assert.Equal(false, result); } [Fact] @@ -50,8 +47,7 @@ namespace Perspex.Markup.UnitTests.Binding var target = new ExpressionObserver(data, "!Foo"); var result = await target.Take(1); - Assert.True(result.HasValue); - Assert.Equal(true, result.Value); + Assert.Equal(true, result); } [Fact] @@ -61,8 +57,7 @@ namespace Perspex.Markup.UnitTests.Binding var target = new ExpressionObserver(data, "!Foo"); var result = await target.Take(1); - Assert.True(result.HasValue); - Assert.Equal(false, result.Value); + Assert.Equal(false, result); } [Fact] @@ -72,7 +67,7 @@ namespace Perspex.Markup.UnitTests.Binding var target = new ExpressionObserver(data, "!Foo"); var result = await target.Take(1); - Assert.False(result.HasValue); + Assert.Equal(PerspexProperty.UnsetValue, result); } [Fact] @@ -82,7 +77,7 @@ namespace Perspex.Markup.UnitTests.Binding var target = new ExpressionObserver(data, "!Foo"); var result = await target.Take(1); - Assert.False(result.HasValue); + Assert.Equal(PerspexProperty.UnsetValue, result); } [Fact] diff --git a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Observable.cs b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Observable.cs index 979fa3856c..9296b5aa7c 100644 --- a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Observable.cs +++ b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Observable.cs @@ -22,11 +22,11 @@ namespace Perspex.Markup.UnitTests.Binding var target = new ExpressionObserver(data, "Foo"); var result = new List(); - var sub = target.Subscribe(x => result.Add(x.Value)); + var sub = target.Subscribe(x => result.Add(x)); source.OnNext("bar"); sync.ExecutePostedCallbacks(); - Assert.Equal(new[] { null, "foo", "bar" }, result); + Assert.Equal(new[] { PerspexProperty.UnsetValue, "foo", "bar" }, result); } } @@ -39,11 +39,11 @@ namespace Perspex.Markup.UnitTests.Binding var target = new ExpressionObserver(data, "Next.Foo"); var result = new List(); - var sub = target.Subscribe(x => result.Add(x.Value)); + var sub = target.Subscribe(x => result.Add(x)); data.Next.OnNext(new Class2("foo")); sync.ExecutePostedCallbacks(); - Assert.Equal(new[] { null, "foo" }, result); + Assert.Equal(new[] { PerspexProperty.UnsetValue, "foo" }, result); sub.Dispose(); Assert.Equal(0, data.SubscriptionCount); diff --git a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Property.cs b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Property.cs index 3518cfa52d..a782578505 100644 --- a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Property.cs +++ b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Property.cs @@ -18,8 +18,7 @@ namespace Perspex.Markup.UnitTests.Binding var target = new ExpressionObserver(data, "Foo"); var result = await target.Take(1); - Assert.True(result.HasValue); - Assert.Equal("foo", result.Value); + Assert.Equal("foo", result); } [Fact] @@ -29,8 +28,7 @@ namespace Perspex.Markup.UnitTests.Binding var target = new ExpressionObserver(data, "Foo"); var result = await target.Take(1); - Assert.True(result.HasValue); - Assert.Equal("foo", result.Value); + Assert.Equal("foo", result); } [Fact] @@ -40,8 +38,7 @@ namespace Perspex.Markup.UnitTests.Binding var target = new ExpressionObserver(data, "Foo.Bar.Baz"); var result = await target.Take(1); - Assert.True(result.HasValue); - Assert.Equal("baz", result.Value); + Assert.Equal("baz", result); } [Fact] @@ -51,7 +48,7 @@ namespace Perspex.Markup.UnitTests.Binding var target = new ExpressionObserver(data, "Foo.Bar.Baz"); var result = await target.Take(1); - Assert.False(result.HasValue); + Assert.Equal(PerspexProperty.UnsetValue, result); } [Fact] @@ -61,7 +58,7 @@ namespace Perspex.Markup.UnitTests.Binding var target = new ExpressionObserver(data, "Foo"); var result = new List(); - var sub = target.Subscribe(x => result.Add(x.Value)); + var sub = target.Subscribe(x => result.Add(x)); data.Foo = "bar"; Assert.Equal(new[] { "foo", "bar" }, result); @@ -78,7 +75,7 @@ namespace Perspex.Markup.UnitTests.Binding var target = new ExpressionObserver(data, "Next.Bar"); var result = new List(); - var sub = target.Subscribe(x => result.Add(x.Value)); + var sub = target.Subscribe(x => result.Add(x)); ((Class2)data.Next).Bar = "baz"; Assert.Equal(new[] { "bar", "baz" }, result); @@ -96,7 +93,7 @@ namespace Perspex.Markup.UnitTests.Binding var target = new ExpressionObserver(data, "Next.Bar"); var result = new List(); - var sub = target.Subscribe(x => result.Add(x.Value)); + var sub = target.Subscribe(x => result.Add(x)); var old = data.Next; data.Next = new Class2 { Bar = "baz" }; @@ -116,12 +113,12 @@ namespace Perspex.Markup.UnitTests.Binding var target = new ExpressionObserver(data, "Next.Bar"); var result = new List(); - var sub = target.Subscribe(x => result.Add(x.Value)); + var sub = target.Subscribe(x => result.Add(x)); var old = data.Next; data.Next = null; data.Next = new Class2 { Bar = "baz" }; - Assert.Equal(new[] { "bar", null, "baz" }, result); + Assert.Equal(new[] { "bar", PerspexProperty.UnsetValue, "baz" }, result); sub.Dispose(); @@ -137,13 +134,13 @@ namespace Perspex.Markup.UnitTests.Binding var target = new ExpressionObserver(data, "Next.Bar"); var result = new List(); - var sub = target.Subscribe(x => result.Add(x.Value)); + var sub = target.Subscribe(x => result.Add(x)); var old = data.Next; var breaking = new WithoutBar(); data.Next = breaking; data.Next = new Class2 { Bar = "baz" }; - Assert.Equal(new[] { "bar", null, "baz" }, result); + Assert.Equal(new[] { "bar", PerspexProperty.UnsetValue, "baz" }, result); sub.Dispose(); @@ -206,7 +203,7 @@ namespace Perspex.Markup.UnitTests.Binding var target = new ExpressionObserver(null, "Foo"); var result = await target.Take(1); - Assert.False(result.HasValue); + Assert.Equal(PerspexProperty.UnsetValue, result); } [Fact] @@ -217,11 +214,11 @@ namespace Perspex.Markup.UnitTests.Binding var target = new ExpressionObserver(first, "Foo"); var result = new List(); - var sub = target.Subscribe(x => result.Add(x.Value)); + var sub = target.Subscribe(x => result.Add(x)); target.Root = second; target.Root = null; - Assert.Equal(new[] { "foo", "bar", null }, result); + Assert.Equal(new[] { "foo", "bar", PerspexProperty.UnsetValue }, result); Assert.Equal(0, first.SubscriptionCount); Assert.Equal(0, second.SubscriptionCount); diff --git a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Task.cs b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Task.cs index 6ab8b88415..7f6e153a5f 100644 --- a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Task.cs +++ b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Task.cs @@ -23,11 +23,11 @@ namespace Perspex.Markup.UnitTests.Binding var target = new ExpressionObserver(data, "Foo"); var result = new List(); - var sub = target.Subscribe(x => result.Add(x.Value)); + var sub = target.Subscribe(x => result.Add(x)); tcs.SetResult("foo"); sync.ExecutePostedCallbacks(); - Assert.Equal(new object[] { null, "foo" }, result.ToArray()); + Assert.Equal(new object[] { PerspexProperty.UnsetValue, "foo" }, result.ToArray()); } } @@ -40,7 +40,7 @@ namespace Perspex.Markup.UnitTests.Binding var target = new ExpressionObserver(data, "Foo"); var result = new List(); - var sub = target.Subscribe(x => result.Add(x.Value)); + var sub = target.Subscribe(x => result.Add(x)); Assert.Equal(new object[] { "foo" }, result.ToArray()); } @@ -56,11 +56,11 @@ namespace Perspex.Markup.UnitTests.Binding var target = new ExpressionObserver(data, "Next.Foo"); var result = new List(); - var sub = target.Subscribe(x => result.Add(x.Value)); + var sub = target.Subscribe(x => result.Add(x)); tcs.SetResult(new Class2("foo")); sync.ExecutePostedCallbacks(); - Assert.Equal(new object[] { null, "foo" }, result.ToArray()); + Assert.Equal(new object[] { PerspexProperty.UnsetValue, "foo" }, result.ToArray()); } } diff --git a/tests/Perspex.Styling.UnitTests/SelectorTests_Child.cs b/tests/Perspex.Styling.UnitTests/SelectorTests_Child.cs index 1a682b28b1..0df2e061a3 100644 --- a/tests/Perspex.Styling.UnitTests/SelectorTests_Child.cs +++ b/tests/Perspex.Styling.UnitTests/SelectorTests_Child.cs @@ -88,6 +88,14 @@ namespace Perspex.Styling.UnitTests public ITemplatedControl TemplatedParent { get; } + public IPropertyBag InheritanceParent + { + get + { + throw new NotImplementedException(); + } + } + public IDisposable Bind(PerspexProperty property, IObservable source, BindingPriority priority) { throw new NotImplementedException(); diff --git a/tests/Perspex.Styling.UnitTests/SelectorTests_Descendent.cs b/tests/Perspex.Styling.UnitTests/SelectorTests_Descendent.cs index 3eb660f8a8..7b5f78e6bb 100644 --- a/tests/Perspex.Styling.UnitTests/SelectorTests_Descendent.cs +++ b/tests/Perspex.Styling.UnitTests/SelectorTests_Descendent.cs @@ -120,6 +120,14 @@ namespace Perspex.Styling.UnitTests public ITemplatedControl TemplatedParent { get; } + public IPropertyBag InheritanceParent + { + get + { + throw new NotImplementedException(); + } + } + public IDisposable Bind(PerspexProperty property, IObservable source, BindingPriority priority) { throw new NotImplementedException(); diff --git a/tests/Perspex.Styling.UnitTests/TestControlBase.cs b/tests/Perspex.Styling.UnitTests/TestControlBase.cs index 3c1612ee76..ad47f1eb27 100644 --- a/tests/Perspex.Styling.UnitTests/TestControlBase.cs +++ b/tests/Perspex.Styling.UnitTests/TestControlBase.cs @@ -28,6 +28,14 @@ namespace Perspex.Styling.UnitTests set; } + public IPropertyBag InheritanceParent + { + get + { + throw new NotImplementedException(); + } + } + public IDisposable Bind(PerspexProperty property, IObservable source, BindingPriority priority) { throw new NotImplementedException(); diff --git a/tests/Perspex.Styling.UnitTests/TestTemplatedControl.cs b/tests/Perspex.Styling.UnitTests/TestTemplatedControl.cs index 6b4d32d2ef..764c402138 100644 --- a/tests/Perspex.Styling.UnitTests/TestTemplatedControl.cs +++ b/tests/Perspex.Styling.UnitTests/TestTemplatedControl.cs @@ -34,6 +34,14 @@ namespace Perspex.Styling.UnitTests get; } + public IPropertyBag InheritanceParent + { + get + { + throw new NotImplementedException(); + } + } + public IObservable GetObservable(PerspexProperty property) { throw new NotImplementedException(); From 12c1127f060cd7dd129d4d1e395610f5402b8a92 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 3 Oct 2015 11:13:44 +0200 Subject: [PATCH 62/72] Check array bounds in expression indexer. --- .../Perspex.Markup/Binding/IndexerNode.cs | 27 ++++++++++++++++- .../ExpressionObserverTests_Indexer.cs | 30 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/Markup/Perspex.Markup/Binding/IndexerNode.cs b/src/Markup/Perspex.Markup/Binding/IndexerNode.cs index f29dcce741..5f1d840fbe 100644 --- a/src/Markup/Perspex.Markup/Binding/IndexerNode.cs +++ b/src/Markup/Perspex.Markup/Binding/IndexerNode.cs @@ -90,7 +90,12 @@ namespace Perspex.Markup.Binding if (typeInfo.IsArray && _intArgs != null) { - return ((Array)target).GetValue(_intArgs); + var array = (Array)target; + + if (InBounds(_intArgs, array)) + { + return array.GetValue(_intArgs); + } } else if (target is IList && _intArgs?.Length == 1) { @@ -102,5 +107,25 @@ namespace Perspex.Markup.Binding return PerspexProperty.UnsetValue; } + + private bool InBounds(int[] args, Array array) + { + if (args.Length == array.Rank) + { + for (var i = 0; i < args.Length; ++i) + { + if (args[i] >= array.GetLength(i)) + { + return false; + } + } + + return true; + } + else + { + return false; + } + } } } diff --git a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Indexer.cs b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Indexer.cs index 36bc2afbde..97d9bd1ccc 100644 --- a/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Indexer.cs +++ b/tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Indexer.cs @@ -32,6 +32,36 @@ namespace Perspex.Markup.UnitTests.Binding Assert.Equal("qux", result); } + [Fact] + public async void Array_Out_Of_Bounds_Should_Return_UnsetValue() + { + var data = new { Foo = new[] { "foo", "bar" } }; + var target = new ExpressionObserver(data, "Foo[2]"); + var result = await target.Take(1); + + Assert.Equal(PerspexProperty.UnsetValue, result); + } + + [Fact] + public async void Array_With_Wrong_Dimensions_Should_Return_UnsetValue() + { + var data = new { Foo = new[] { "foo", "bar" } }; + var target = new ExpressionObserver(data, "Foo[1,2]"); + var result = await target.Take(1); + + Assert.Equal(PerspexProperty.UnsetValue, result); + } + + [Fact] + public async void List_Out_Of_Bounds_Should_Return_UnsetValue() + { + var data = new { Foo = new List { "foo", "bar" } }; + var target = new ExpressionObserver(data, "Foo[2]"); + var result = await target.Take(1); + + Assert.Equal(PerspexProperty.UnsetValue, result); + } + [Fact] public async void Should_Get_List_Value() { From 843eb65dea8dc4d462dc7fd575fc0955f9d3c9b1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 3 Oct 2015 11:49:05 +0200 Subject: [PATCH 63/72] Bind DataContext to Parent.DataContext. Need to special-case binding to DataContext as previously it was trying to bind to itself. When binding to DataContext, we're binding to the *parent* DataContext. --- .../Binding/XamlBinding.cs | 41 ++++++++++++++----- .../Binding/XamlBindingTests.cs | 30 ++++++++++++++ 2 files changed, 61 insertions(+), 10 deletions(-) diff --git a/src/Markup/Perspex.Markup.Xaml/Binding/XamlBinding.cs b/src/Markup/Perspex.Markup.Xaml/Binding/XamlBinding.cs index 2bf9b82bcf..cb2c999f7a 100644 --- a/src/Markup/Perspex.Markup.Xaml/Binding/XamlBinding.cs +++ b/src/Markup/Perspex.Markup.Xaml/Binding/XamlBinding.cs @@ -29,21 +29,42 @@ namespace Perspex.Markup.Xaml.Binding public void Bind(IObservablePropertyBag instance, PerspexProperty property) { - if (property == Control.DataContextProperty && instance.InheritanceParent != null) + var subject = new ExpressionSubject(CreateExpressionObserver(instance, property)); + + if (subject != null) { - instance = instance.InheritanceParent as IObservablePropertyBag ?? instance; + Bind(instance, property, subject); } - - var subject = new ExpressionSubject(CreateExpressionObserver(instance)); - Bind(instance, property, subject); } - public ExpressionObserver CreateExpressionObserver(IObservablePropertyBag instance) + public ExpressionObserver CreateExpressionObserver( + IObservablePropertyBag instance, + PerspexProperty property) { - var result = new ExpressionObserver(null, SourcePropertyPath); - var dataContext = instance.GetObservable(Control.DataContextProperty); - dataContext.Subscribe(x => result.Root = x); - return result; + IObservable dataContext = null; + + if (property != Control.DataContextProperty) + { + dataContext = instance.GetObservable(Control.DataContextProperty); + } + else + { + var parent = instance.InheritanceParent as IObservablePropertyBag; + + if (parent != null) + { + dataContext = parent.GetObservable(Control.DataContextProperty); + } + } + + if (dataContext != null) + { + var result = new ExpressionObserver(null, SourcePropertyPath); + dataContext.Subscribe(x => result.Root = x); + return result; + } + + return null; } internal void Bind(IObservablePropertyBag target, PerspexProperty property, ISubject subject) diff --git a/tests/Perspex.Markup.Xaml.UnitTests/Binding/XamlBindingTests.cs b/tests/Perspex.Markup.Xaml.UnitTests/Binding/XamlBindingTests.cs index 97e0dc7741..f78a7ed4ae 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/Binding/XamlBindingTests.cs +++ b/tests/Perspex.Markup.Xaml.UnitTests/Binding/XamlBindingTests.cs @@ -113,6 +113,36 @@ namespace Perspex.Markup.Xaml.UnitTests.Binding BindingPriority.LocalValue)); } + [Fact] + public void DataContext_Binding_Should_Use_Parent_DataContext() + { + var parentDataContext = Mock.Of(x => x.Header == (object)"Foo"); + + var parent = new Decorator + { + Child = new Control(), + DataContext = parentDataContext, + }; + + var binding = new XamlBinding + { + SourcePropertyPath = "Header", + }; + + binding.Bind(parent.Child, Control.DataContextProperty); + + Assert.Equal("Foo", parent.Child.DataContext); + + parentDataContext = Mock.Of(x => x.Header == (object)"Bar"); + parent.DataContext = parentDataContext; + Assert.Equal("Bar", parent.Child.DataContext); + } + + private Mock CreateTarget(object dataContext) + { + return CreateTarget(dataContext: Observable.Never().StartWith(dataContext)); + } + private Mock CreateTarget( IObservable dataContext = null, IObservable text = null) From a47da04a0b9f58a26dcc2b1c4fc4f22a5462935e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 3 Oct 2015 12:35:13 +0200 Subject: [PATCH 64/72] Updated OmniXAML --- src/Markup/Perspex.Markup.Xaml/OmniXAML | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Markup/Perspex.Markup.Xaml/OmniXAML b/src/Markup/Perspex.Markup.Xaml/OmniXAML index 42b0e3b6ef..d33ff9879d 160000 --- a/src/Markup/Perspex.Markup.Xaml/OmniXAML +++ b/src/Markup/Perspex.Markup.Xaml/OmniXAML @@ -1 +1 @@ -Subproject commit 42b0e3b6efc0905457120752be38a6000898fffa +Subproject commit d33ff9879dd2347cc4472d22a92946846b9386cc From 730bdc88cbf3aa46b138f4ae0a5dffd65ba146ec Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 3 Oct 2015 13:16:01 +0200 Subject: [PATCH 65/72] Output XML docs for Perspex.Markup. --- src/Markup/Perspex.Markup/Perspex.Markup.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Markup/Perspex.Markup/Perspex.Markup.csproj b/src/Markup/Perspex.Markup/Perspex.Markup.csproj index 1f2da5171e..e16d0f1deb 100644 --- a/src/Markup/Perspex.Markup/Perspex.Markup.csproj +++ b/src/Markup/Perspex.Markup/Perspex.Markup.csproj @@ -24,6 +24,7 @@ DEBUG;TRACE prompt 4 + bin\Debug\Perspex.Markup.XML pdbonly From a400a890039712e1aec1d0e50c5d2a94697f9d37 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 3 Oct 2015 13:43:39 +0200 Subject: [PATCH 66/72] Handle invalid values in direct property bindings. Invalid values will be converted to default(TPropertyValue). --- src/Perspex.Base/PerspexObject.cs | 4 +- src/Perspex.Base/Utilities/TypeUtilities.cs | 32 +++++++++++- .../PerspexObjectTests_Direct.cs | 50 ++++++++++++++++++- 3 files changed, 82 insertions(+), 4 deletions(-) diff --git a/src/Perspex.Base/PerspexObject.cs b/src/Perspex.Base/PerspexObject.cs index 03d5fecd69..696d01893a 100644 --- a/src/Perspex.Base/PerspexObject.cs +++ b/src/Perspex.Base/PerspexObject.cs @@ -605,7 +605,9 @@ namespace Perspex property, GetDescription(source)); - return source.Subscribe(x => SetValue(property, x)); + return source + .Select(x => TypeUtilities.CastOrDefault(x, property.PropertyType, false)) + .Subscribe(x => SetValue(property, x)); } else { diff --git a/src/Perspex.Base/Utilities/TypeUtilities.cs b/src/Perspex.Base/Utilities/TypeUtilities.cs index 98909d9801..d026efc622 100644 --- a/src/Perspex.Base/Utilities/TypeUtilities.cs +++ b/src/Perspex.Base/Utilities/TypeUtilities.cs @@ -32,8 +32,9 @@ namespace Perspex.Utilities /// The type to cast to. /// The value to cast. /// If sucessful, contains the cast value. + /// Allow . /// True if the cast was sucessful, otherwise false. - public static bool TryCast(Type to, object value, out object result) + public static bool TryCast(Type to, object value, out object result, bool allowUnset = true) { Contract.Requires(to != null); @@ -46,7 +47,7 @@ namespace Perspex.Utilities var from = value.GetType(); - if (value == PerspexProperty.UnsetValue) + if (allowUnset && value == PerspexProperty.UnsetValue) { result = value; return true; @@ -77,5 +78,32 @@ namespace Perspex.Utilities result = null; return false; } + + /// + /// Casts a value to a type, returning the default for that type if the value could not be + /// cast. + /// + /// The value to cast. + /// The type to cast to.. + /// Allow . + /// A value of . + public static object CastOrDefault(object value, Type type, bool allowUnset = true) + { + var typeInfo = type.GetTypeInfo(); + object result; + + if (TypeUtilities.TryCast(type, value, out result, allowUnset)) + { + return result; + } + else if (typeInfo.IsValueType) + { + return Activator.CreateInstance(type); + } + else + { + return null; + } + } } } diff --git a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs index c8b4470d13..a9a2d5c1fc 100644 --- a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs +++ b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs @@ -148,6 +148,45 @@ namespace Perspex.Base.UnitTests Assert.Equal("second", target.Foo); } + [Fact] + public void Bind_Handles_Wrong_Type() + { + var target = new Class1(); + var source = new Subject(); + + var sub = target.Bind(Class1.FooProperty, source); + + source.OnNext(45); + + Assert.Equal(null, target.Foo); + } + + [Fact] + public void Bind_Handles_Wrong_Value_Type() + { + var target = new Class1(); + var source = new Subject(); + + var sub = target.Bind(Class1.BazProperty, source); + + source.OnNext("foo"); + + Assert.Equal(0, target.Baz); + } + + [Fact] + public void Bind_Handles_UnsetValue() + { + var target = new Class1(); + var source = new Subject(); + + var sub = target.Bind(Class1.BazProperty, source); + + source.OnNext(PerspexProperty.UnsetValue); + + Assert.Equal(0, target.Baz); + } + [Fact] public void ReadOnly_Property_Cannot_Be_Set() { @@ -295,9 +334,12 @@ namespace Perspex.Base.UnitTests public static readonly PerspexProperty BarProperty = PerspexProperty.RegisterDirect("Bar", o => o.Bar); - private string _foo = "initial"; + public static readonly PerspexProperty BazProperty = + PerspexProperty.RegisterDirect("Bar", o => o.Baz, (o,v) => o.Baz = v); + private string _foo = "initial"; private string _bar = "bar"; + private int _baz = 5; public string Foo { @@ -309,6 +351,12 @@ namespace Perspex.Base.UnitTests { get { return _bar; } } + + public int Baz + { + get { return _baz; } + set { SetAndRaise(BazProperty, ref _baz, value); } + } } private class Class2 : PerspexObject From b26fbd160e74dfa9f820e9b78d801e18f94d2682 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 3 Oct 2015 14:10:37 +0200 Subject: [PATCH 67/72] Updated OmniXAML. --- src/Markup/Perspex.Markup.Xaml/OmniXAML | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Markup/Perspex.Markup.Xaml/OmniXAML b/src/Markup/Perspex.Markup.Xaml/OmniXAML index d33ff9879d..2a6309a4d9 160000 --- a/src/Markup/Perspex.Markup.Xaml/OmniXAML +++ b/src/Markup/Perspex.Markup.Xaml/OmniXAML @@ -1 +1 @@ -Subproject commit d33ff9879dd2347cc4472d22a92946846b9386cc +Subproject commit 2a6309a4d9b60b848241a34bf2adfa16b52c7a85 From 9a8ef44084776302c2f2a967f4da9c510d21833b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 3 Oct 2015 14:11:03 +0200 Subject: [PATCH 68/72] Use invariant culture when parsing numbers. --- src/Markup/Perspex.Markup/Binding/Parsers/LiteralParser.cs | 3 ++- src/Perspex.SceneGraph/Media/Color.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Markup/Perspex.Markup/Binding/Parsers/LiteralParser.cs b/src/Markup/Perspex.Markup/Binding/Parsers/LiteralParser.cs index 08056f4aab..3026be551c 100644 --- a/src/Markup/Perspex.Markup/Binding/Parsers/LiteralParser.cs +++ b/src/Markup/Perspex.Markup/Binding/Parsers/LiteralParser.cs @@ -1,6 +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 System.Globalization; using System.Text; namespace Perspex.Markup.Binding.Parsers @@ -25,7 +26,7 @@ namespace Perspex.Markup.Binding.Parsers } } - return int.Parse(result.ToString()); + return int.Parse(result.ToString(), CultureInfo.InvariantCulture); } return null; diff --git a/src/Perspex.SceneGraph/Media/Color.cs b/src/Perspex.SceneGraph/Media/Color.cs index 9a474ef210..35856ddb63 100644 --- a/src/Perspex.SceneGraph/Media/Color.cs +++ b/src/Perspex.SceneGraph/Media/Color.cs @@ -106,7 +106,7 @@ namespace Perspex.Media throw new FormatException($"Invalid color string: '{s}'."); } - return FromUInt32(uint.Parse(s.Substring(1), NumberStyles.HexNumber) | or); + return FromUInt32(uint.Parse(s.Substring(1), NumberStyles.HexNumber, CultureInfo.InvariantCulture) | or); } else { From ad1b92cd77c3c420d4b29c00bccf0a4b248b9f30 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 3 Oct 2015 15:01:39 +0200 Subject: [PATCH 69/72] Implemented ClearContainers for TreeView. --- .../Generators/TreeItemContainerGenerator.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs b/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs index 4f35084d70..d4aad6236e 100644 --- a/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs @@ -16,7 +16,7 @@ namespace Perspex.Controls.Generators /// The type of the container. public class TreeItemContainerGenerator : ITreeItemContainerGenerator where T : TreeViewItem, new() { - private readonly Dictionary _containers = new Dictionary(); + private Dictionary _containers = new Dictionary(); private readonly Subject _containersInitialized = new Subject(); @@ -102,7 +102,9 @@ namespace Perspex.Controls.Generators /// The removed controls. public IList ClearContainers() { - throw new NotImplementedException(); + var result = _containers; + _containers = new Dictionary(); + return result.Values.Cast().ToList(); } /// From 92e6274ec3136a61787072b27b5b7cdd92de8129 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 3 Oct 2015 16:32:50 +0200 Subject: [PATCH 70/72] Fixed input hit test Z ordering. Closes #170. --- src/Perspex.Input/IInputElement.cs | 8 +- src/Perspex.Input/InputElement.cs | 11 -- src/Perspex.Input/InputExtensions.cs | 59 +++++++- src/Perspex.SceneGraph/Visual.cs | 5 + .../InputElement_HitTesting.cs | 129 ++++++++++++++++++ .../Perspex.Input.UnitTests.csproj | 1 + 6 files changed, 194 insertions(+), 19 deletions(-) create mode 100644 tests/Perspex.Input.UnitTests/InputElement_HitTesting.cs diff --git a/src/Perspex.Input/IInputElement.cs b/src/Perspex.Input/IInputElement.cs index d6e5378f02..e8658c99b0 100644 --- a/src/Perspex.Input/IInputElement.cs +++ b/src/Perspex.Input/IInputElement.cs @@ -114,14 +114,8 @@ namespace Perspex.Input void Focus(); /// - /// Returns the input element that can be found within the current control at the specified - /// position. + /// Gets the key bindings for the element. /// - /// The position, in control coordinates. - /// The at the specified position. - IInputElement InputHitTest(Point p); - - List KeyBindings { get; } } } diff --git a/src/Perspex.Input/InputElement.cs b/src/Perspex.Input/InputElement.cs index 43cfc840c6..7e0907c9e8 100644 --- a/src/Perspex.Input/InputElement.cs +++ b/src/Perspex.Input/InputElement.cs @@ -339,17 +339,6 @@ namespace Perspex.Input public List KeyBindings { get; } = new List(); - /// - /// Returns the input element that can be found within the current control at the specified - /// position. - /// - /// The position, in control coordinates. - /// The at the specified position. - public IInputElement InputHitTest(Point p) - { - return this.GetInputElementsAt(p).FirstOrDefault(); - } - /// /// Focuses the control. /// diff --git a/src/Perspex.Input/InputExtensions.cs b/src/Perspex.Input/InputExtensions.cs index ef3f848073..50d826b2a9 100644 --- a/src/Perspex.Input/InputExtensions.cs +++ b/src/Perspex.Input/InputExtensions.cs @@ -7,8 +7,19 @@ using System.Linq; namespace Perspex.Input { + /// + /// Defines extensions for the interface. + /// public static class InputExtensions { + /// + /// Returns the active input elements at a point on an . + /// + /// The element to test. + /// The point on . + /// + /// The active input elements found at the point, ordered topmost first. + /// public static IEnumerable GetInputElementsAt(this IInputElement element, Point p) { Contract.Requires(element != null); @@ -22,7 +33,7 @@ namespace Perspex.Input if (element.VisualChildren.Any()) { - foreach (var child in element.VisualChildren.OfType()) + foreach (var child in ZSort(element.VisualChildren.OfType())) { foreach (var result in child.GetInputElementsAt(p)) { @@ -34,5 +45,51 @@ namespace Perspex.Input yield return element; } } + + /// + /// Returns the topmost active input element at a point on an . + /// + /// The element to test. + /// The point on . + /// The topmost at the specified position. + public static IInputElement InputHitTest(this IInputElement element, Point p) + { + return element.GetInputElementsAt(p).First(); + } + + private static IEnumerable ZSort(IEnumerable elements) + { + return elements + .Select((element, index) => new ZOrderElement + { + Element = element, + Index = index, + ZIndex = element.ZIndex, + }) + .OrderBy(x => x, null) + .Select(x => x.Element); + + } + + private class ZOrderElement : IComparable + { + public IInputElement Element { get; set; } + public int Index { get; set; } + public int ZIndex { get; set; } + + public int CompareTo(ZOrderElement other) + { + var z = other.ZIndex - ZIndex; + + if (z != 0) + { + return z; + } + else + { + return other.Index - Index; + } + } + } } } diff --git a/src/Perspex.SceneGraph/Visual.cs b/src/Perspex.SceneGraph/Visual.cs index e835c6a76c..9050f76a4f 100644 --- a/src/Perspex.SceneGraph/Visual.cs +++ b/src/Perspex.SceneGraph/Visual.cs @@ -192,6 +192,11 @@ namespace Perspex /// /// Gets the Z index of the node. /// + /// + /// Controls with a higher will appear in front of controls with + /// a lower ZIndex. If two controls have the same ZIndex then the control that appears + /// later in the containing element's children collection will appear on top. + /// public int ZIndex { get { return GetValue(ZIndexProperty); } diff --git a/tests/Perspex.Input.UnitTests/InputElement_HitTesting.cs b/tests/Perspex.Input.UnitTests/InputElement_HitTesting.cs new file mode 100644 index 0000000000..aeb11aa413 --- /dev/null +++ b/tests/Perspex.Input.UnitTests/InputElement_HitTesting.cs @@ -0,0 +1,129 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Perspex.Controls; +using Perspex.Layout; +using Xunit; + +namespace Perspex.Input.UnitTests +{ + public class InputElement_HitTesting + { + [Fact] + public void InputHitTest_Should_Find_Control_At_Point() + { + var container = new Decorator + { + Width = 200, + Height = 200, + Child = new Border + { + Width = 100, + Height = 100, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + } + }; + + container.Measure(Size.Infinity); + container.Arrange(new Rect(container.DesiredSize)); + + var result = container.InputHitTest(new Point(100, 100)); + + Assert.Equal(container.Child, result); + } + + [Fact] + public void InputHitTest_Should_Not_Find_Control_Outside_Point() + { + var container = new Decorator + { + Width = 200, + Height = 200, + Child = new Border + { + Width = 100, + Height = 100, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + } + }; + + container.Measure(Size.Infinity); + container.Arrange(new Rect(container.DesiredSize)); + + var result = container.InputHitTest(new Point(10, 10)); + + Assert.Equal(container, result); + } + + [Fact] + public void InputHitTest_Should_Find_Top_Control_At_Point() + { + var container = new Panel + { + Width = 200, + Height = 200, + Children = new Controls.Controls + { + new Border + { + Width = 100, + Height = 100, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }, + new Border + { + Width = 50, + Height = 50, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + } + } + }; + + container.Measure(Size.Infinity); + container.Arrange(new Rect(container.DesiredSize)); + + var result = container.InputHitTest(new Point(100, 100)); + + Assert.Equal(container.Children[1], result); + } + + [Fact] + public void InputHitTest_Should_Find_Top_Control_At_Point_With_ZOrder() + { + var container = new Panel + { + Width = 200, + Height = 200, + Children = new Controls.Controls + { + new Border + { + Width = 100, + Height = 100, + ZIndex = 1, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }, + new Border + { + Width = 50, + Height = 50, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + } + } + }; + + container.Measure(Size.Infinity); + container.Arrange(new Rect(container.DesiredSize)); + + var result = container.InputHitTest(new Point(100, 100)); + + Assert.Equal(container.Children[0], result); + } + } +} diff --git a/tests/Perspex.Input.UnitTests/Perspex.Input.UnitTests.csproj b/tests/Perspex.Input.UnitTests/Perspex.Input.UnitTests.csproj index 1aff689782..051e2a966f 100644 --- a/tests/Perspex.Input.UnitTests/Perspex.Input.UnitTests.csproj +++ b/tests/Perspex.Input.UnitTests/Perspex.Input.UnitTests.csproj @@ -55,6 +55,7 @@ + From d183cf1bc335f6b4f55653601d5a851d7f729cac Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 3 Oct 2015 17:09:02 +0200 Subject: [PATCH 71/72] Fix XamlTestApplication. --- samples/XamlTestApplicationPcl/Views/MainWindow.paml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/XamlTestApplicationPcl/Views/MainWindow.paml b/samples/XamlTestApplicationPcl/Views/MainWindow.paml index 149948db44..3b6433d5b4 100644 --- a/samples/XamlTestApplicationPcl/Views/MainWindow.paml +++ b/samples/XamlTestApplicationPcl/Views/MainWindow.paml @@ -1,7 +1,7 @@  From 5b65becdef8e03b0c768e4933432c4274a2d44c8 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 5 Oct 2015 14:21:12 +0300 Subject: [PATCH 72/72] Changed matrix multiplication order --- src/Gtk/Perspex.Cairo/Media/DrawingContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gtk/Perspex.Cairo/Media/DrawingContext.cs b/src/Gtk/Perspex.Cairo/Media/DrawingContext.cs index f1255b9ce1..a1fee1a2e3 100644 --- a/src/Gtk/Perspex.Cairo/Media/DrawingContext.cs +++ b/src/Gtk/Perspex.Cairo/Media/DrawingContext.cs @@ -140,7 +140,7 @@ namespace Perspex.Cairo.Media var impl = geometry.PlatformImpl as StreamGeometryImpl; var oldMatrix = Transform; - Transform *= impl.Transform; + Transform = impl.Transform * Transform; if (brush != null)