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 @@
+