Browse Source

Added ElementName bindings.

pull/325/head
Steven Kirk 11 years ago
parent
commit
7865743819
  1. 7
      samples/BindingTest/MainWindow.paml
  2. 67
      src/Markup/Perspex.Markup.Xaml/Data/Binding.cs
  3. 2
      src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs
  4. 4
      src/Markup/Perspex.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs
  5. 81
      src/Markup/Perspex.Markup/Data/ExpressionObserver.cs
  6. 2
      src/Perspex.SceneGraph/NameScope.cs
  7. 28
      tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs
  8. 1
      tests/Perspex.Markup.UnitTests/packages.config
  9. 83
      tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests_ElementName.cs
  10. 2
      tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj
  11. 2
      tests/Perspex.Markup.Xaml.UnitTests/SamplePerspexObject.cs
  12. 61
      tests/Perspex.Markup.Xaml.UnitTests/TestRoot.cs
  13. 2
      tests/Perspex.Markup.Xaml.UnitTests/TypeProviderMock.cs
  14. 2
      tests/Perspex.Markup.Xaml.UnitTests/ViewModelMock.cs

7
samples/BindingTest/MainWindow.paml

@ -6,7 +6,7 @@
<StackPanel Orientation="Horizontal">
<StackPanel Margin="18" Gap="4" Width="200">
<TextBlock FontSize="16" Text="Simple Bindings"/>
<TextBox Watermark="Two Way" UseFloatingWatermark="True" Text="{Binding Path=StringValue}"/>
<TextBox Watermark="Two Way" UseFloatingWatermark="True" Text="{Binding Path=StringValue}" Name="first"/>
<TextBox Watermark="One Way" UseFloatingWatermark="True" Text="{Binding Path=StringValue, Mode=OneWay}"/>
<TextBox Watermark="One Time" UseFloatingWatermark="True" Text="{Binding Path=StringValue, Mode=OneTime}"/>
<TextBox Watermark="One Way To Source" UseFloatingWatermark="True" Text="{Binding Path=StringValue, Mode=OneWayToSource}"/>
@ -30,6 +30,11 @@
<TextBlock Text="{Binding Path=DoubleValue}"/>
<ProgressBar Maximum="10" Value="{Binding DoubleValue}"/>
</StackPanel>
<StackPanel Margin="18" Gap="4" Width="200" HorizontalAlignment="Left">
<TextBlock FontSize="16" Text="Binding Sources"/>
<TextBox Watermark="Value of first TextBox" UseFloatingWatermark="True"
Text="{Binding Path=Text, ElementName=first, Mode=TwoWay}"/>
</StackPanel>
</StackPanel>
</StackPanel>
</TabItem>

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

@ -4,7 +4,6 @@
using System;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using OmniXaml.TypeConversion;
using Perspex.Controls;
using Perspex.Markup.Data;
@ -20,6 +19,11 @@ namespace Perspex.Markup.Xaml.Data
/// </summary>
public IValueConverter Converter { get; set; }
/// <summary>
/// Gets or sets the name of the element to use as the binding source.
/// </summary>
public string ElementName { get; set; }
/// <summary>
/// Gets or sets the binding mode.
/// </summary>
@ -47,8 +51,11 @@ namespace Perspex.Markup.Xaml.Data
/// <param name="property">The target property.</param>
public void Bind(IObservablePropertyBag instance, PerspexProperty property)
{
Contract.Requires<ArgumentNullException>(instance != null);
Contract.Requires<ArgumentNullException>(property != null);
var subject = CreateSubject(
instance,
instance,
property.PropertyType,
property == Control.DataContextProperty);
@ -72,15 +79,22 @@ namespace Perspex.Markup.Xaml.Data
Type targetType,
bool targetIsDataContext = false)
{
Contract.Requires<ArgumentNullException>(target != null);
Contract.Requires<ArgumentNullException>(targetType != null);
ExpressionObserver observer;
if (RelativeSource == null || RelativeSource.Mode == RelativeSourceMode.DataContext)
if (ElementName != null)
{
observer = CreateElementSubject((IControl)target);
}
else if (RelativeSource == null || RelativeSource.Mode == RelativeSourceMode.DataContext)
{
observer = CreateDataContextExpressionSubject(target, targetIsDataContext);
observer = CreateDataContextSubject(target, targetIsDataContext);
}
else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent)
{
observer = CreateTemplatedParentExpressionSubject(target);
observer = CreateTemplatedParentSubject(target);
}
else
{
@ -88,8 +102,8 @@ namespace Perspex.Markup.Xaml.Data
}
return new ExpressionSubject(
observer,
targetType,
observer,
targetType,
Converter ?? DefaultValueConverter.Instance);
}
@ -101,6 +115,10 @@ namespace Perspex.Markup.Xaml.Data
/// <param name="subject">The binding subject.</param>
internal void Bind(IObservablePropertyBag target, PerspexProperty property, ISubject<object> subject)
{
Contract.Requires<ArgumentNullException>(target != null);
Contract.Requires<ArgumentNullException>(property != null);
Contract.Requires<ArgumentNullException>(subject != null);
var mode = Mode == BindingMode.Default ?
property.DefaultBindingMode : Mode;
@ -117,7 +135,7 @@ namespace Perspex.Markup.Xaml.Data
target.GetObservable(Control.DataContextProperty).Subscribe(dataContext =>
{
subject.Take(1).Subscribe(x => target.SetValue(property, x, Priority));
});
});
break;
case BindingMode.OneWayToSource:
target.GetObservable(property).Subscribe(subject);
@ -125,10 +143,12 @@ namespace Perspex.Markup.Xaml.Data
}
}
private ExpressionObserver CreateDataContextExpressionSubject(
private ExpressionObserver CreateDataContextSubject(
IObservablePropertyBag target,
bool targetIsDataContext)
{
Contract.Requires<ArgumentNullException>(target != null);
var dataContextHost = targetIsDataContext ?
target.InheritanceParent as IObservablePropertyBag : target;
@ -148,8 +168,10 @@ namespace Perspex.Markup.Xaml.Data
}
}
private ExpressionObserver CreateTemplatedParentExpressionSubject(IObservablePropertyBag target)
private ExpressionObserver CreateTemplatedParentSubject(IObservablePropertyBag target)
{
Contract.Requires<ArgumentNullException>(target != null);
var result = new ExpressionObserver(
() => target.GetValue(Control.TemplatedParentProperty),
GetExpression());
@ -167,6 +189,31 @@ namespace Perspex.Markup.Xaml.Data
return result;
}
private ExpressionObserver CreateElementSubject(IControl target)
{
Contract.Requires<ArgumentNullException>(target != null);
var result = new ExpressionObserver(
ControlLocator.Track(target, ElementName),
GetExpression());
return result;
}
private IControl LookupNamedControl(IControl target)
{
Contract.Requires<ArgumentNullException>(target != null);
var nameScope = target.FindNameScope();
if (nameScope == null)
{
throw new InvalidOperationException(
"Could not find name scope for ElementName binding.");
}
return nameScope.Find<IControl>(ElementName);
}
private string GetExpression()
{
return SourcePropertyPath == null || SourcePropertyPath == "." ?

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

@ -22,12 +22,14 @@ namespace Perspex.Markup.Xaml.MarkupExtensions
return new Binding
{
Converter = Converter,
ElementName = ElementName,
Mode = Mode,
SourcePropertyPath = Path,
};
}
public IValueConverter Converter { get; set; }
public string ElementName { get; set; }
public BindingMode Mode { get; set; }
public string Path { get; set; }
}

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

@ -19,9 +19,10 @@ namespace Perspex.Markup.Xaml.MarkupExtensions
public override object ProvideValue(MarkupExtensionContext extensionContext)
{
return new Data.Binding
return new Binding
{
Converter = Converter,
ElementName = ElementName,
Mode = Mode,
Priority = BindingPriority.TemplatedParent,
RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),
@ -30,6 +31,7 @@ namespace Perspex.Markup.Xaml.MarkupExtensions
}
public IValueConverter Converter { get; set; }
public string ElementName { get; set; }
public BindingMode Mode { get; set; }
public string Path { get; set; }
}

81
src/Markup/Perspex.Markup/Data/ExpressionObserver.cs

@ -26,9 +26,12 @@ namespace Perspex.Markup.Data
new InpcPropertyAccessorPlugin(),
};
private Func<object> _root;
private readonly object _root;
private readonly Func<object> _rootGetter;
private readonly IObservable<object> _rootObservable;
private IDisposable _rootObserverSubscription;
private int _count;
private ExpressionNode _node;
private readonly ExpressionNode _node;
private ISubject<object> _empty;
/// <summary>
@ -37,21 +40,50 @@ namespace Perspex.Markup.Data
/// <param name="root">The root object.</param>
/// <param name="expression">The expression.</param>
public ExpressionObserver(object root, string expression)
: this(() => root, expression)
{
Contract.Requires<ArgumentNullException>(expression != null);
_root = root;
if (!string.IsNullOrWhiteSpace(expression))
{
_node = ExpressionNodeBuilder.Build(expression);
}
Expression = expression;
}
/// <summary>
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="root">A function which gets the root object.</param>
/// <param name="rootObservable">An observable which provides the root object.</param>
/// <param name="expression">The expression.</param>
public ExpressionObserver(Func<object> root, string expression)
public ExpressionObserver(IObservable<object> rootObservable, string expression)
{
Contract.Requires<ArgumentNullException>(root != null);
Contract.Requires<ArgumentNullException>(rootObservable != null);
Contract.Requires<ArgumentNullException>(expression != null);
_root = root;
_rootObservable = rootObservable;
if (!string.IsNullOrWhiteSpace(expression))
{
_node = ExpressionNodeBuilder.Build(expression);
}
Expression = expression;
}
/// <summary>
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
/// </summary>
/// <param name="rootGetter">A function which gets the root object.</param>
/// <param name="expression">The expression.</param>
public ExpressionObserver(Func<object> rootGetter, string expression)
{
Contract.Requires<ArgumentNullException>(rootGetter != null);
Contract.Requires<ArgumentNullException>(expression != null);
_rootGetter = rootGetter;
if (!string.IsNullOrWhiteSpace(expression))
{
@ -105,9 +137,13 @@ namespace Perspex.Markup.Data
{
return (Leaf as PropertyAccessorNode)?.PropertyType;
}
else if(_rootGetter != null)
{
return _rootGetter()?.GetType();
}
else
{
return _root()?.GetType();
return _root?.GetType();
}
}
finally
@ -134,19 +170,19 @@ namespace Perspex.Markup.Data
}
/// <summary>
/// Causes the root object to be re-read.
/// Causes the root object to be re-read from the root getter.
/// </summary>
public void UpdateRoot()
{
if (_count > 0)
if (_count > 0 && _rootGetter != null)
{
if (_node != null)
{
_node.Target = _root();
_node.Target = _rootGetter();
}
else if (_empty != null)
{
_empty.OnNext(_root());
_empty.OnNext(_rootGetter());
}
}
}
@ -170,7 +206,7 @@ namespace Perspex.Markup.Data
{
if (_empty == null)
{
_empty = new BehaviorSubject<object>(_root());
_empty = new BehaviorSubject<object>(_rootGetter());
}
return _empty.Subscribe(observer);
@ -181,7 +217,18 @@ namespace Perspex.Markup.Data
{
if (_count++ == 0 && _node != null)
{
_node.Target = _root();
if (_rootGetter != null)
{
_node.Target = _rootGetter();
}
else if (_rootObservable != null)
{
_rootObserverSubscription = _rootObservable.Subscribe(x => _node.Target = x);
}
else
{
_node.Target = _root;
}
}
}
@ -189,6 +236,12 @@ namespace Perspex.Markup.Data
{
if (--_count == 0 && _node != null)
{
if (_rootObserverSubscription != null)
{
_rootObserverSubscription.Dispose();
_rootObserverSubscription = null;
}
_node.Target = null;
}
}

2
src/Perspex.SceneGraph/NameScope.cs

@ -17,7 +17,7 @@ namespace Perspex
public static readonly PerspexProperty<INameScope> NameScopeProperty =
PerspexProperty.RegisterAttached<NameScope, Visual, INameScope>("NameScope");
private Dictionary<string, object> _inner = new Dictionary<string, object>();
private readonly Dictionary<string, object> _inner = new Dictionary<string, object>();
/// <summary>
/// Raised when an element is registered with the name scope.

28
tests/Perspex.Markup.UnitTests/Data/ExpressionObserverTests_Property.cs

@ -3,8 +3,9 @@
using System;
using System.Collections.Generic;
using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Microsoft.Reactive.Testing;
using Perspex.Markup.Data;
using Xunit;
@ -203,6 +204,26 @@ namespace Perspex.Markup.UnitTests.Data
Assert.Equal(new[] { "foo", "bar" }, result);
}
[Fact]
public void Should_Track_Property_Value_From_Observable_Root()
{
var scheduler = new TestScheduler();
var source = scheduler.CreateColdObservable(
OnNext(1, new Class1 { Foo = "foo" }),
OnNext(2, new Class1 { Foo = "bar" }));
var target = new ExpressionObserver(source, "Foo");
var result = new List<object>();
using (target.Subscribe(x => result.Add(x)))
{
scheduler.Start();
}
Assert.Equal(new[] { PerspexProperty.UnsetValue, "foo", "bar" }, result);
Assert.Equal(1, source.Subscriptions.Count);
Assert.NotEqual(Subscription.Infinite, source.Subscriptions[0].Unsubscribe);
}
[Fact]
public void SetValue_Should_Set_Simple_Property_Value()
{
@ -333,5 +354,10 @@ namespace Perspex.Markup.UnitTests.Data
private class WithoutBar : NotifyingBase, INext
{
}
public Recorded<Notification<object>> OnNext(long time, object value)
{
return new Recorded<Notification<object>>(time, Notification.CreateOnNext<object>(value));
}
}
}

1
tests/Perspex.Markup.UnitTests/packages.config

@ -5,6 +5,7 @@
<package id="Rx-Linq" version="2.2.5" targetFramework="net46" />
<package id="Rx-Main" version="2.2.5" targetFramework="net46" />
<package id="Rx-PlatformServices" version="2.2.5" targetFramework="net46" />
<package id="Rx-Testing" version="2.2.5" targetFramework="net46" />
<package id="xunit" version="2.0.0" targetFramework="net46" />
<package id="xunit.abstractions" version="2.0.0" targetFramework="net46" />
<package id="xunit.assert" version="2.0.0" targetFramework="net46" />

83
tests/Perspex.Markup.Xaml.UnitTests/Data/BindingTests_ElementName.cs

@ -0,0 +1,83 @@
// 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.Data;
using Xunit;
namespace Perspex.Markup.Xaml.UnitTests.Data
{
public class BindingTests_ElementName
{
[Fact]
public void Should_Bind_To_Element()
{
TextBlock target;
var root = new TestRoot
{
Child = new StackPanel
{
Children = new Controls.Controls
{
new TextBlock
{
Name = "source",
Text = "foo",
},
(target = new TextBlock
{
Name = "target",
})
}
}
};
var binding = new Binding
{
ElementName = "source",
SourcePropertyPath = "Text",
};
binding.Bind(target, TextBlock.TextProperty);
Assert.Equal("foo", target.Text);
}
[Fact]
public void Should_Bind_To_Later_Added_Element()
{
TextBlock target;
StackPanel stackPanel;
var root = new TestRoot
{
Child = stackPanel = new StackPanel
{
Children = new Controls.Controls
{
(target = new TextBlock
{
Name = "target",
}),
}
}
};
var binding = new Binding
{
ElementName = "source",
SourcePropertyPath = "Text",
};
binding.Bind(target, TextBlock.TextProperty);
stackPanel.Children.Add(new TextBlock
{
Name = "source",
Text = "foo",
});
Assert.Equal("foo", target.Text);
}
}
}

2
tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj

@ -88,6 +88,7 @@
<Otherwise />
</Choose>
<ItemGroup>
<Compile Include="Data\BindingTests_ElementName.cs" />
<Compile Include="Data\MultiBindingTests.cs" />
<Compile Include="Data\BindingTests_TemplatedParent.cs" />
<Compile Include="Data\BindingTests.cs" />
@ -104,6 +105,7 @@
<Compile Include="SampleModel\UserRepositoriesViewModel.cs" />
<Compile Include="SamplePerspexObject.cs" />
<Compile Include="Templates\DataTemplateTests.cs" />
<Compile Include="TestRoot.cs" />
<Compile Include="TypeProviderMock.cs" />
<Compile Include="ViewModelMock.cs" />
</ItemGroup>

2
tests/Perspex.Markup.Xaml.UnitTests/SamplePerspexObject.cs

@ -3,7 +3,7 @@
using System;
namespace Perspex.Xaml.Base.UnitTest
namespace Perspex.Markup.Xaml.UnitTests
{
internal class SamplePerspexObject : PerspexObject
{

61
tests/Perspex.Markup.Xaml.UnitTests/TestRoot.cs

@ -0,0 +1,61 @@
// 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.Platform;
using Perspex.Rendering;
namespace Perspex.Markup.Xaml.UnitTests
{
public class TestRoot : Decorator, IRenderRoot, INameScope
{
private readonly NameScope _nameScope = new NameScope();
event EventHandler<NameScopeEventArgs> INameScope.Registered
{
add { _nameScope.Registered += value; ++NameScopeRegisteredSubscribers; }
remove { _nameScope.Registered -= value; --NameScopeRegisteredSubscribers; }
}
public event EventHandler<NameScopeEventArgs> Unregistered
{
add { _nameScope.Unregistered += value; ++NameScopeUnregisteredSubscribers; }
remove { _nameScope.Unregistered -= value; --NameScopeUnregisteredSubscribers; }
}
public int NameScopeRegisteredSubscribers { get; private set; }
public int NameScopeUnregisteredSubscribers { get; private set; }
public IRenderTarget RenderTarget
{
get { throw new NotImplementedException(); }
}
public IRenderQueueManager RenderQueueManager
{
get { throw new NotImplementedException(); }
}
public Point TranslatePointToScreen(Point p)
{
throw new NotImplementedException();
}
public void Register(string name, object element)
{
_nameScope.Register(name, element);
}
public object Find(string name)
{
return _nameScope.Find(name);
}
public void Unregister(string name)
{
_nameScope.Unregister(name);
}
}
}

2
tests/Perspex.Markup.Xaml.UnitTests/TypeProviderMock.cs

@ -4,7 +4,7 @@
using OmniXaml;
using System;
namespace Perspex.Xaml.Base.UnitTest
namespace Perspex.Markup.Xaml.UnitTests
{
internal class TypeProviderMock : ITypeProvider
{

2
tests/Perspex.Markup.Xaml.UnitTests/ViewModelMock.cs

@ -4,7 +4,7 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace Perspex.Xaml.Base.UnitTest
namespace Perspex.Markup.Xaml.UnitTests
{
internal class ViewModelMock : INotifyPropertyChanged
{

Loading…
Cancel
Save