Browse Source

Make TreeViewItem.IsExpanded bindings work.

pull/396/head
Steven Kirk 11 years ago
parent
commit
3e6402711c
  1. 34
      samples/XamlTestApplicationPcl/ViewModels/MainWindowViewModel.cs
  2. 11
      samples/XamlTestApplicationPcl/ViewModels/TestNode.cs
  3. 9
      samples/XamlTestApplicationPcl/Views/MainWindow.paml
  4. 16
      samples/XamlTestApplicationPcl/XamlTestApplicationPcl.csproj
  5. 5
      samples/XamlTestApplicationPcl/packages.config
  6. 12
      src/Markup/Perspex.Markup.Xaml/Data/Binding.cs
  7. 2
      src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs
  8. 3
      src/Markup/Perspex.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs
  9. 2
      src/Perspex.Base/Perspex.Base.csproj
  10. 51
      src/Perspex.Base/PerspexObjectExtensions.cs
  11. 16
      src/Perspex.Base/Reactive/AnonymousSubject`1.cs
  12. 49
      src/Perspex.Base/Reactive/AnonymousSubject`2.cs
  13. 1
      src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs
  14. 15
      src/Perspex.Controls/Primitives/ToggleButton.cs
  15. 31
      src/Perspex.Styling/Styling/ActivatedSubject.cs
  16. 9
      src/Perspex.Styling/Styling/Setter.cs
  17. 87
      tests/Perspex.Markup.Xaml.UnitTests/StyleTests.cs
  18. 70
      tests/Perspex.Styling.UnitTests/ActivatedSubjectTests.cs

34
samples/XamlTestApplicationPcl/ViewModels/MainWindowViewModel.cs

@ -1,7 +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.
using System;
using System.Collections.Generic;
using ReactiveUI;
namespace XamlTestApplication.ViewModels
{
@ -22,6 +24,7 @@ namespace XamlTestApplication.ViewModels
{
Header = "Root",
SubHeader = "Root Item",
IsExpanded = true,
Children = new[]
{
new TestNode
@ -33,6 +36,7 @@ namespace XamlTestApplication.ViewModels
{
Header = "Child 2",
SubHeader = "Child 2 Value",
IsExpanded = false,
Children = new[]
{
new TestNode
@ -50,9 +54,39 @@ namespace XamlTestApplication.ViewModels
}
}
};
CollapseNodesCommand = ReactiveCommand.Create();
CollapseNodesCommand.Subscribe(_ => ExpandNodes(false));
ExpandNodesCommand = ReactiveCommand.Create();
ExpandNodesCommand.Subscribe(_ => ExpandNodes(true));
}
public List<TestItem> Items { get; }
public List<TestNode> Nodes { get; }
public ReactiveCommand<object> CollapseNodesCommand { get; }
public ReactiveCommand<object> ExpandNodesCommand { get; }
public void ExpandNodes(bool expanded)
{
foreach (var node in Nodes)
{
ExpandNodes(node, expanded);
}
}
private void ExpandNodes(TestNode node, bool expanded)
{
node.IsExpanded = expanded;
if (node.Children != null)
{
foreach (var child in node.Children)
{
ExpandNodes(child, expanded);
}
}
}
}
}

11
samples/XamlTestApplicationPcl/ViewModels/TestNode.cs

@ -2,13 +2,22 @@
// 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.ViewModels
{
public class TestNode
public class TestNode : ReactiveObject
{
private bool _isExpanded;
public string Header { get; set; }
public string SubHeader { get; set; }
public IEnumerable<TestNode> Children { get; set; }
public bool IsExpanded
{
get { return _isExpanded; }
set { this.RaiseAndSetIfChanged(ref this._isExpanded, value); }
}
}
}

9
samples/XamlTestApplicationPcl/Views/MainWindow.paml

@ -147,6 +147,11 @@
</StackPanel>
</DropDown>
<TreeView Items="{Binding Nodes}">
<TreeView.Styles>
<Style Selector="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
</Style>
</TreeView.Styles>
<TreeView.DataTemplates>
<TreeDataTemplate DataType="vm:TestNode" ItemsSource="{Binding Children}">
<StackPanel>
@ -156,6 +161,10 @@
</TreeDataTemplate>
</TreeView.DataTemplates>
</TreeView>
<StackPanel Orientation="Vertical" Gap="4">
<Button Command="{Binding CollapseNodesCommand}">Collapse Nodes</Button>
<Button Command="{Binding ExpandNodesCommand}">Expand Nodes</Button>
</StackPanel>
</StackPanel>
</TabItem>
<TabItem Header="Layout">

16
samples/XamlTestApplicationPcl/XamlTestApplicationPcl.csproj

@ -123,6 +123,22 @@
<HintPath>..\..\packages\Splat.1.6.2\lib\Portable-net45+win+wpa81+wp80\Splat.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Reactive.Core, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\packages\Rx-Core.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Core.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Reactive.Interfaces, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\packages\Rx-Interfaces.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Interfaces.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Reactive.Linq, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\packages\Rx-Linq.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Linq.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Reactive.PlatformServices, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\packages\Rx-PlatformServices.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.PlatformServices.dll</HintPath>
<Private>True</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="XamlTestApp.paml">

5
samples/XamlTestApplicationPcl/packages.config

@ -1,4 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Rx-Core" version="2.2.5" targetFramework="portable45-net45+win8" />
<package id="Rx-Interfaces" version="2.2.5" targetFramework="portable45-net45+win8" />
<package id="Rx-Linq" version="2.2.5" targetFramework="portable45-net45+win8" />
<package id="Rx-Main" version="2.2.5" targetFramework="portable45-net45+win8" />
<package id="Rx-PlatformServices" version="2.2.5" targetFramework="portable45-net45+win8" />
<package id="Splat" version="1.6.2" targetFramework="portable45-net45+win8" />
</packages>

12
src/Markup/Perspex.Markup.Xaml/Data/Binding.cs

@ -71,21 +71,21 @@ namespace Perspex.Markup.Xaml.Data
if (pathInfo.ElementName != null || ElementName != null)
{
observer = CreateElementSubject(
observer = CreateElementObserver(
(IControl)target,
pathInfo.ElementName ?? ElementName,
pathInfo.Path);
}
else if (RelativeSource == null || RelativeSource.Mode == RelativeSourceMode.DataContext)
{
observer = CreateDataContextSubject(
observer = CreateDataContexObserver(
target,
pathInfo.Path,
targetIsDataContext);
}
else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent)
{
observer = CreateTemplatedParentSubject(
observer = CreateTemplatedParentObserver(
target,
pathInfo.Path);
}
@ -148,7 +148,7 @@ namespace Perspex.Markup.Xaml.Data
}
}
private ExpressionObserver CreateDataContextSubject(
private ExpressionObserver CreateDataContexObserver(
IPerspexObject target,
string path,
bool targetIsDataContext)
@ -178,7 +178,7 @@ namespace Perspex.Markup.Xaml.Data
}
}
private ExpressionObserver CreateTemplatedParentSubject(
private ExpressionObserver CreateTemplatedParentObserver(
IPerspexObject target,
string path)
{
@ -196,7 +196,7 @@ namespace Perspex.Markup.Xaml.Data
return result;
}
private ExpressionObserver CreateElementSubject(
private ExpressionObserver CreateElementObserver(
IControl target,
string elementName,
string path)

2
src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs

@ -27,6 +27,7 @@ namespace Perspex.Markup.Xaml.MarkupExtensions
ElementName = ElementName,
Mode = Mode,
Path = Path,
Priority = Priority,
};
}
@ -35,5 +36,6 @@ namespace Perspex.Markup.Xaml.MarkupExtensions
public string ElementName { get; set; }
public BindingMode Mode { get; set; }
public string Path { get; set; }
public BindingPriority Priority { get; set; } = BindingPriority.LocalValue;
}
}

3
src/Markup/Perspex.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs

@ -25,9 +25,9 @@ namespace Perspex.Markup.Xaml.MarkupExtensions
Converter = Converter,
ElementName = ElementName,
Mode = Mode,
Priority = BindingPriority.TemplatedParent,
RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),
Path = Path,
Priority = Priority,
};
}
@ -35,5 +35,6 @@ namespace Perspex.Markup.Xaml.MarkupExtensions
public string ElementName { get; set; }
public BindingMode Mode { get; set; }
public string Path { get; set; }
public BindingPriority Priority { get; set; } = BindingPriority.TemplatedParent;
}
}

2
src/Perspex.Base/Perspex.Base.csproj

@ -76,6 +76,8 @@
<Compile Include="PriorityLevel.cs" />
<Compile Include="PriorityValue.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Reactive\AnonymousSubject`1.cs" />
<Compile Include="Reactive\AnonymousSubject`2.cs" />
<Compile Include="Reactive\PerspexObservable.cs" />
<Compile Include="Threading\Dispatcher.cs" />
<Compile Include="Threading\DispatcherPriority.cs" />

51
src/Perspex.Base/PerspexObjectExtensions.cs

@ -121,7 +121,7 @@ namespace Perspex
BindingPriority priority = BindingPriority.LocalValue)
{
// TODO: Subject.Create<T> is not yet in stable Rx : once it is, remove the
// AnonymousSubject classes from this file and use Subject.Create<T>.
// AnonymousSubject classes and use Subject.Create<T>.
var output = new Subject<object>();
var result = new AnonymousSubject<object>(
Observer.Create<object>(
@ -272,54 +272,5 @@ namespace Perspex
handler(target)(e);
}
}
class AnonymousSubject<T, U> : ISubject<T, U>
{
private readonly IObserver<T> _observer;
private readonly IObservable<U> _observable;
public AnonymousSubject(IObserver<T> observer, IObservable<U> observable)
{
_observer = observer;
_observable = observable;
}
public void OnCompleted()
{
_observer.OnCompleted();
}
public void OnError(Exception error)
{
if (error == null)
throw new ArgumentNullException("error");
_observer.OnError(error);
}
public void OnNext(T value)
{
_observer.OnNext(value);
}
public IDisposable Subscribe(IObserver<U> observer)
{
if (observer == null)
throw new ArgumentNullException("observer");
//
// [OK] Use of unsafe Subscribe: non-pretentious wrapping of an observable sequence.
//
return _observable.Subscribe/*Unsafe*/(observer);
}
}
class AnonymousSubject<T> : AnonymousSubject<T, T>, ISubject<T>
{
public AnonymousSubject(IObserver<T> observer, IObservable<T> observable)
: base(observer, observable)
{
}
}
}
}

16
src/Perspex.Base/Reactive/AnonymousSubject`1.cs

@ -0,0 +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.Reactive.Subjects;
namespace Perspex.Reactive
{
public class AnonymousSubject<T> : AnonymousSubject<T, T>, ISubject<T>
{
public AnonymousSubject(IObserver<T> observer, IObservable<T> observable)
: base(observer, observable)
{
}
}
}

49
src/Perspex.Base/Reactive/AnonymousSubject`2.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.Reactive.Subjects;
namespace Perspex.Reactive
{
public class AnonymousSubject<T, U> : ISubject<T, U>
{
private readonly IObserver<T> _observer;
private readonly IObservable<U> _observable;
public AnonymousSubject(IObserver<T> observer, IObservable<U> observable)
{
_observer = observer;
_observable = observable;
}
public void OnCompleted()
{
_observer.OnCompleted();
}
public void OnError(Exception error)
{
if (error == null)
throw new ArgumentNullException("error");
_observer.OnError(error);
}
public void OnNext(T value)
{
_observer.OnNext(value);
}
public IDisposable Subscribe(IObserver<U> observer)
{
if (observer == null)
throw new ArgumentNullException("observer");
//
// [OK] Use of unsafe Subscribe: non-pretentious wrapping of an observable sequence.
//
return _observable.Subscribe/*Unsafe*/(observer);
}
}
}

1
src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs

@ -77,7 +77,6 @@ namespace Perspex.Controls.Generators
result.SetValue(ContentProperty, template.Build(item));
result.SetValue(ItemsProperty, template.ItemsSelector(item));
result.SetValue(IsExpandedProperty, template.IsExpanded(item));
if (!(item is IControl))
{

15
src/Perspex.Controls/Primitives/ToggleButton.cs

@ -9,21 +9,22 @@ namespace Perspex.Controls.Primitives
public class ToggleButton : Button
{
public static readonly PerspexProperty<bool> IsCheckedProperty =
PerspexProperty.Register<ToggleButton, bool>("IsChecked");
PerspexProperty.RegisterDirect<ToggleButton, bool>(
"IsChecked",
o => o.IsChecked,
(o,v) => o.IsChecked = v);
private bool _isChecked;
static ToggleButton()
{
PseudoClass(IsCheckedProperty, ":checked");
}
public ToggleButton()
{
}
public bool IsChecked
{
get { return GetValue(IsCheckedProperty); }
set { SetValue(IsCheckedProperty, value); }
get { return _isChecked; }
set { SetAndRaise(IsCheckedProperty, ref _isChecked, value); }
}
protected override void OnClick(RoutedEventArgs e)

31
src/Perspex.Styling/Styling/ActivatedSubject.cs

@ -19,7 +19,8 @@ namespace Perspex.Styling
/// </remarks>
internal class ActivatedSubject : ActivatedObservable, ISubject<object>, IDescription
{
private bool _active;
private bool? _active;
private bool _completed;
private object _value;
/// <summary>
@ -34,7 +35,7 @@ namespace Perspex.Styling
string description)
: base(activator, source, description)
{
Activator.Skip(1).Subscribe(ActivatorChanged);
Activator.Subscribe(ActivatorChanged, ActivatorError, ActivatorCompleted);
}
/// <summary>
@ -50,7 +51,7 @@ namespace Perspex.Styling
/// </summary>
public void OnCompleted()
{
if (_active)
if (_active.Value && !_completed)
{
Source.OnCompleted();
}
@ -63,7 +64,7 @@ namespace Perspex.Styling
/// <exception cref="ArgumentNullException"><paramref name="error"/> is null.</exception>
public void OnError(Exception error)
{
if (_active)
if (_active.Value && !_completed)
{
Source.OnError(error);
}
@ -77,7 +78,7 @@ namespace Perspex.Styling
{
_value = value;
if (_active)
if (_active.Value && !_completed)
{
Source.OnNext(value);
}
@ -85,8 +86,26 @@ namespace Perspex.Styling
private void ActivatorChanged(bool active)
{
bool first = !_active.HasValue;
_active = active;
Source.OnNext(active ? _value : PerspexProperty.UnsetValue);
if (!first)
{
Source.OnNext(active ? _value : PerspexProperty.UnsetValue);
}
}
private void ActivatorCompleted()
{
_completed = true;
Source.OnCompleted();
}
private void ActivatorError(Exception e)
{
_completed = true;
Source.OnError(e);
}
}
}

9
src/Perspex.Styling/Styling/Setter.cs

@ -5,6 +5,7 @@ using System;
using System.Reactive.Subjects;
using Perspex.Data;
using Perspex.Metadata;
using Perspex.Reactive;
namespace Perspex.Styling
{
@ -63,6 +64,10 @@ namespace Perspex.Styling
/// <param name="activator">An optional activator.</param>
public void Apply(IStyle style, IStyleable control, IObservable<bool> activator)
{
Contract.Requires<ArgumentNullException>(control != null);
var description = style?.ToString();
if (Property == null)
{
throw new InvalidOperationException("Setter.Property must be set.");
@ -79,7 +84,7 @@ namespace Perspex.Styling
else
{
var subject = binding.CreateSubject(control, Property);
var activated = new ActivatedSubject(activator, subject, style.ToString());
var activated = new ActivatedSubject(activator, subject, description);
Bind(control, Property, binding, activated);
}
}
@ -91,7 +96,7 @@ namespace Perspex.Styling
}
else
{
var activated = new ActivatedValue(activator, Value, style.ToString());
var activated = new ActivatedValue(activator, Value, description);
control.Bind(Property, activated, BindingPriority.StyleTrigger);
}
}

87
tests/Perspex.Markup.Xaml.UnitTests/StyleTests.cs

@ -2,7 +2,11 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Linq;
using System.Reactive.Linq;
using Moq;
using Perspex.Controls;
using Perspex.Controls.Primitives;
using Perspex.Data;
using Perspex.Markup.Xaml.Data;
using Perspex.Platform;
using Perspex.Styling;
@ -29,5 +33,88 @@ namespace Perspex.Markup.Xaml.UnitTests
Assert.IsType<Binding>(setter.Value);
}
}
[Fact]
public void Setter_With_TwoWay_Binding_Should_Update_Source()
{
using (PerspexLocator.EnterScope())
{
PerspexLocator.CurrentMutable
.Bind<IPlatformThreadingInterface>()
.ToConstant(Mock.Of<IPlatformThreadingInterface>(x =>
x.CurrentThreadIsLoopThread == true));
var data = new Data
{
Foo = "foo",
};
var control = new TextBox
{
DataContext = data,
};
var setter = new Setter
{
Property = TextBox.TextProperty,
Value = new Binding
{
Path = "Foo",
Mode = BindingMode.TwoWay
}
};
setter.Apply(null, control, null);
Assert.Equal("foo", control.Text);
control.Text = "bar";
Assert.Equal("bar", data.Foo);
}
}
[Fact]
public void Setter_With_TwoWay_Binding_And_Activator_Should_Update_Source()
{
using (PerspexLocator.EnterScope())
{
PerspexLocator.CurrentMutable
.Bind<IPlatformThreadingInterface>()
.ToConstant(Mock.Of<IPlatformThreadingInterface>(x =>
x.CurrentThreadIsLoopThread == true));
var data = new Data
{
Foo = "foo",
};
var control = new TextBox
{
DataContext = data,
};
var setter = new Setter
{
Property = TextBox.TextProperty,
Value = new Binding
{
Path = "Foo",
Mode = BindingMode.TwoWay
}
};
var activator = Observable.Never<bool>().StartWith(true);
setter.Apply(null, control, activator);
Assert.Equal("foo", control.Text);
control.Text = "bar";
Assert.Equal("bar", data.Foo);
}
}
private class Data
{
public string Foo { get; set; }
}
}
}

70
tests/Perspex.Styling.UnitTests/ActivatedSubjectTests.cs

@ -1,8 +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.
using System;
using System.Reactive.Disposables;
using System.Reactive.Subjects;
using Perspex.Data;
using Xunit;
namespace Perspex.Styling.UnitTests
@ -12,23 +13,44 @@ namespace Perspex.Styling.UnitTests
[Fact]
public void Should_Set_Values()
{
var data = new Class1 { Foo = "foo" };
var activator = new BehaviorSubject<bool>(false);
var source = data.GetSubject(
(PerspexProperty)Class1.FooProperty,
BindingPriority.LocalValue);
var source = new TestSubject();
var target = new ActivatedSubject(activator, source, string.Empty);
target.OnNext("bar");
Assert.Equal("foo", data.Foo);
Assert.Equal(PerspexProperty.UnsetValue, source.Value);
activator.OnNext(true);
target.OnNext("baz");
Assert.Equal("baz", data.Foo);
Assert.Equal("baz", source.Value);
activator.OnNext(false);
Assert.Equal("foo", data.Foo);
Assert.Equal(PerspexProperty.UnsetValue, source.Value);
target.OnNext("bax");
activator.OnNext(true);
Assert.Equal("bax", data.Foo);
Assert.Equal("bax", source.Value);
}
[Fact]
public void Should_Invoke_OnCompleted_On_Activator_Completed()
{
var activator = new BehaviorSubject<bool>(false);
var source = new TestSubject();
var target = new ActivatedSubject(activator, source, string.Empty);
activator.OnCompleted();
Assert.True(source.Completed);
}
[Fact]
public void Should_Invoke_OnError_On_Activator_Error()
{
var activator = new BehaviorSubject<bool>(false);
var source = new TestSubject();
var target = new ActivatedSubject(activator, source, string.Empty);
activator.OnError(new Exception());
Assert.NotNull(source.Error);
}
private class Class1 : PerspexObject
@ -42,5 +64,35 @@ namespace Perspex.Styling.UnitTests
set { SetValue(FooProperty, value); }
}
}
private class TestSubject : ISubject<object>
{
private IObserver<object> _observer;
public bool Completed { get; set; }
public Exception Error { get; set; }
public object Value { get; set; } = PerspexProperty.UnsetValue;
public void OnCompleted()
{
Completed = true;
}
public void OnError(Exception error)
{
Error = error;
}
public void OnNext(object value)
{
Value = value;
}
public IDisposable Subscribe(IObserver<object> observer)
{
_observer = observer;
return Disposable.Empty;
}
}
}
}

Loading…
Cancel
Save