diff --git a/src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs index ceabd09bb7..5de25e4c97 100644 --- a/src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs +++ b/src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs @@ -36,12 +36,12 @@ namespace Perspex.Markup.Xaml.Templates } } - public IEnumerable ItemsSelector(object item) + public InstancedBinding ItemsSelector(object item) { if (ItemsSource != null) { var obs = new ExpressionObserver(item, ItemsSource.Path); - return obs.Take(1).Wait() as IEnumerable; + return new InstancedBinding(obs); } return null; diff --git a/src/Perspex.Base/PerspexObject.cs b/src/Perspex.Base/PerspexObject.cs index 5335410222..37ddf671e2 100644 --- a/src/Perspex.Base/PerspexObject.cs +++ b/src/Perspex.Base/PerspexObject.cs @@ -25,6 +25,17 @@ namespace Perspex /// public class PerspexObject : IPerspexObject, IPerspexObjectDebug, INotifyPropertyChanged { + /// + /// Maintains a list of direct property binding subscriptions so that the binding source + /// doesn't get collected. + /// + /// + /// If/when we provide a ClearBindings() method, then this collection will be need to be + /// moved to an instance field and indexed by property, but until that point a static + /// collection will suffice. + /// + private static List s_directBindings = new List(); + /// /// The parent object that inherited values are inherited from. /// @@ -401,9 +412,20 @@ namespace Perspex property, GetDescription(source)); - return source + IDisposable subscription = null; + + subscription = source .Select(x => TypeUtilities.CastOrDefault(x, property.PropertyType)) + .Do(_ => { }, () => s_directBindings.Remove(subscription)) .Subscribe(x => SetValue(property, x)); + + s_directBindings.Add(subscription); + + return Disposable.Create(() => + { + subscription.Dispose(); + s_directBindings.Remove(subscription); + }); } else { diff --git a/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs b/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs index b6c1f48158..3dd3fa904b 100644 --- a/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using Perspex.Controls.Templates; +using Perspex.Data; namespace Perspex.Controls.Generators { @@ -76,7 +77,7 @@ namespace Perspex.Controls.Generators var result = new T(); result.SetValue(ContentProperty, template.Build(item)); - result.SetValue(ItemsProperty, template.ItemsSelector(item)); + BindingOperations.Apply(result, ItemsProperty, template.ItemsSelector(item), null); if (!(item is IControl)) { diff --git a/src/Perspex.Controls/Templates/FuncTreeDataTemplate.cs b/src/Perspex.Controls/Templates/FuncTreeDataTemplate.cs index 3d17cf5659..59b3115922 100644 --- a/src/Perspex.Controls/Templates/FuncTreeDataTemplate.cs +++ b/src/Perspex.Controls/Templates/FuncTreeDataTemplate.cs @@ -4,6 +4,7 @@ using System; using System.Collections; using System.Reflection; +using Perspex.Data; namespace Perspex.Controls.Templates { @@ -59,9 +60,9 @@ namespace Perspex.Controls.Templates /// /// The item. /// The child items, or null if no child items. - public IEnumerable ItemsSelector(object item) + public InstancedBinding ItemsSelector(object item) { - return this?._itemsSelector(item); + return new InstancedBinding(this?._itemsSelector(item)); } /// diff --git a/src/Perspex.Controls/Templates/ITreeDataTemplate.cs b/src/Perspex.Controls/Templates/ITreeDataTemplate.cs index 6415ed37b2..3b3f4159a0 100644 --- a/src/Perspex.Controls/Templates/ITreeDataTemplate.cs +++ b/src/Perspex.Controls/Templates/ITreeDataTemplate.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 System.Collections; +using Perspex.Data; namespace Perspex.Controls.Templates { @@ -14,7 +14,10 @@ namespace Perspex.Controls.Templates /// Selects the child items of an item. /// /// The item. - /// The child items, or null if no child items. - IEnumerable ItemsSelector(object item); + /// + /// An holding the items, or an observable that tracks the + /// items. May return null if no child items. + /// + InstancedBinding ItemsSelector(object item); } } \ No newline at end of file diff --git a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs index 6c5b43888f..ebf0484ba8 100644 --- a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs +++ b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs @@ -333,6 +333,50 @@ namespace Perspex.Base.UnitTests Assert.Equal("second", target.Foo); } + [Fact] + public void Binding_To_Direct_Property_Does_Not_Get_Collected() + { + var target = new Class2(); + + Func setupBinding = () => + { + var source = new Subject(); + var sub = target.Bind((PerspexProperty)Class1.FooProperty, source); + return new WeakReference(source); + }; + + var weakSource = setupBinding(); + + GC.Collect(); + + Assert.True(weakSource.IsAlive); + } + + [Fact] + public void Binding_To_Direct_Property_Gets_Collected_When_Completed() + { + var target = new Class2(); + + Func setupBinding = () => + { + var source = new Subject(); + var sub = target.Bind((PerspexProperty)Class1.FooProperty, source); + return new WeakReference(source); + }; + + var weakSource = setupBinding(); + + Action completeSource = () => + { + ((ISubject)weakSource.Target).OnCompleted(); + }; + + completeSource(); + GC.Collect(); + + Assert.False(weakSource.IsAlive); + } + [Fact] public void Property_Notifies_Initialized() { diff --git a/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj b/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj index 2888640b07..eb679c3693 100644 --- a/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj +++ b/tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj @@ -148,6 +148,10 @@ {3e53a01a-b331-47f3-b828-4a5717e77a24} Perspex.Markup.Xaml + + {6417e941-21bc-467b-a771-0de389353ce6} + Perspex.Markup + {D211E587-D8BC-45B9-95A4-F297C8FA5200} Perspex.Animation diff --git a/tests/Perspex.Controls.UnitTests/TreeViewTests.cs b/tests/Perspex.Controls.UnitTests/TreeViewTests.cs index fdc86e4a2b..d4e44a44b3 100644 --- a/tests/Perspex.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Perspex.Controls.UnitTests/TreeViewTests.cs @@ -1,13 +1,16 @@ // 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 Perspex.Collections; using Perspex.Controls.Presenters; using Perspex.Controls.Templates; +using Perspex.Data; using Perspex.Input; using Perspex.LogicalTree; +using Perspex.Markup.Data; using Perspex.UnitTests; using Xunit; @@ -252,6 +255,39 @@ namespace Perspex.Controls.UnitTests Assert.NotNull(NameScope.GetNameScope((TreeViewItem)item)); } + [Fact] + public void Should_React_To_Children_Changing() + { + var data = CreateTestTreeData(); + + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = data, + DataTemplates = CreateNodeDataTemplate(), + }; + + ApplyTemplates(target); + + Assert.Equal(new[] { "Root" }, ExtractItemHeader(target, 0)); + Assert.Equal(new[] { "Child1", "Child2" }, ExtractItemHeader(target, 1)); + Assert.Equal(new[] { "Grandchild2a" }, ExtractItemHeader(target, 2)); + + // Make sure that the binding to Node.Children does not get collected. + GC.Collect(); + + data[0].Children = new PerspexList + { + new Node + { + Value = "NewChild1", + } + }; + + Assert.Equal(new[] { "Root" }, ExtractItemHeader(target, 0)); + Assert.Equal(new[] { "NewChild1" }, ExtractItemHeader(target, 1)); + } + private void ApplyTemplates(TreeView tree) { tree.ApplyTemplate(); @@ -303,9 +339,7 @@ namespace Perspex.Controls.UnitTests { return new DataTemplates { - new FuncTreeDataTemplate( - x => new TextBlock { Text = x.Value }, - x => x.Children), + new TestTreeDataTemplate() }; } @@ -360,10 +394,45 @@ namespace Perspex.Controls.UnitTests } } - private class Node + private class Node : NotifyingBase { + private IPerspexList _children; + public string Value { get; set; } - public IPerspexList Children { get; set; } + + public IPerspexList Children + { + get + { + return _children; + } + + set + { + _children = value; + RaisePropertyChanged(nameof(Children)); + } + } + } + + private class TestTreeDataTemplate : ITreeDataTemplate + { + public IControl Build(object param) + { + var node = (Node)param; + return new TextBlock { Text = node.Value }; + } + + public InstancedBinding ItemsSelector(object item) + { + var obs = new ExpressionObserver(item, nameof(Node.Children)); + return new InstancedBinding(obs); + } + + public bool Match(object data) + { + return data is Node; + } } } } diff --git a/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs index 3ae9bb5ef5..35cd4a9917 100644 --- a/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs +++ b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Indexer.cs @@ -8,6 +8,7 @@ using System.Reactive.Linq; using Perspex.Collections; using Perspex.Diagnostics; using Perspex.Markup.Data; +using Perspex.UnitTests; using Xunit; namespace Perspex.Markup.UnitTests.Data diff --git a/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs index bf8803912e..f11c8f25e8 100644 --- a/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs +++ b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Observable.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Reactive.Linq; using System.Reactive.Subjects; using Perspex.Markup.Data; +using Perspex.UnitTests; using Xunit; namespace Perspex.Markup.UnitTests.Data diff --git a/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs index ee3ef3df78..8586f89ab6 100644 --- a/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs +++ b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs @@ -8,6 +8,7 @@ using System.Reactive.Linq; using System.Reactive.Subjects; using Microsoft.Reactive.Testing; using Perspex.Markup.Data; +using Perspex.UnitTests; using Xunit; namespace Perspex.Markup.UnitTests.Data @@ -385,11 +386,6 @@ namespace Perspex.Markup.UnitTests.Data RaisePropertyChanged(nameof(Next)); } } - - public void RaisePropertyChanged(string propertyName) - { - base.RaisePropertyChanged(propertyName); - } } private class Class2 : NotifyingBase, INext diff --git a/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_SetValue.cs b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_SetValue.cs index 166c11eb42..4281a7e590 100644 --- a/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_SetValue.cs +++ b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_SetValue.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Reactive.Linq; using Perspex.Markup.Data; +using Perspex.UnitTests; using Xunit; namespace Perspex.Markup.UnitTests.Data diff --git a/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Task.cs b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Task.cs index b6c39ab28c..0ae2011ad4 100644 --- a/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Task.cs +++ b/tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Task.cs @@ -7,6 +7,7 @@ using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using Perspex.Markup.Data; +using Perspex.UnitTests; using Xunit; namespace Perspex.Markup.UnitTests.Data diff --git a/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj b/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj index 185013373c..f63b05c890 100644 --- a/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj +++ b/tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj @@ -96,7 +96,6 @@ - diff --git a/tests/Perspex.Markup.UnitTests/Data/NotifyingBase.cs b/tests/Perspex.UnitTests/NotifyingBase.cs similarity index 90% rename from tests/Perspex.Markup.UnitTests/Data/NotifyingBase.cs rename to tests/Perspex.UnitTests/NotifyingBase.cs index 384c3ede02..0d2e1c968c 100644 --- a/tests/Perspex.Markup.UnitTests/Data/NotifyingBase.cs +++ b/tests/Perspex.UnitTests/NotifyingBase.cs @@ -4,7 +4,7 @@ using System.ComponentModel; using System.Linq; -namespace Perspex.Markup.UnitTests.Data +namespace Perspex.UnitTests { public class NotifyingBase : INotifyPropertyChanged { @@ -34,7 +34,7 @@ namespace Perspex.Markup.UnitTests.Data private set; } - protected void RaisePropertyChanged(string propertyName) + public void RaisePropertyChanged(string propertyName) { _propertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } diff --git a/tests/Perspex.UnitTests/Perspex.UnitTests.csproj b/tests/Perspex.UnitTests/Perspex.UnitTests.csproj index 675485d81b..21777675eb 100644 --- a/tests/Perspex.UnitTests/Perspex.UnitTests.csproj +++ b/tests/Perspex.UnitTests/Perspex.UnitTests.csproj @@ -62,6 +62,7 @@ +