Browse Source

Merge branch 'grokysmaster' of https://github.com/SuperJMN/Perspex into SuperJMN-grokysmaster

pull/120/head
Steven Kirk 11 years ago
parent
commit
acda05c179
  1. 6
      Tests/Perspex.Markup.Xaml.UnitTests/ChangeBranchTest.cs
  2. 29
      Tests/Perspex.Markup.Xaml.UnitTests/DataContextChangeSynchronizerTest.cs
  3. 2
      Tests/Perspex.Markup.Xaml.UnitTests/XamlBindingTest.cs
  4. 4
      src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs
  5. 53
      src/Markup/Perspex.Markup.Xaml/DataBinding/ChangeTracking/ObservablePropertyBranch.cs
  6. 171
      src/Markup/Perspex.Markup.Xaml/DataBinding/DataContextChangeSynchronizer.cs
  7. 4
      src/Markup/Perspex.Markup.Xaml/DataBinding/PerspexPropertyBinder.cs
  8. 41
      src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBinding.cs

6
Tests/Perspex.Markup.Xaml.UnitTests/ChangeBranchTest.cs

@ -58,12 +58,12 @@
var level1 = new Level1(); var level1 = new Level1();
var branch = new ObservablePropertyBranch(level1, new PropertyPath("Level2.Level3.Property")); var branch = new ObservablePropertyBranch(level1, new PropertyPath("Level2.Level3.Property"));
bool hit = false; bool received = false;
ObservableExtensions.Subscribe(branch.Changed, _ => hit = true); ObservableExtensions.Subscribe(branch.Values, v => received = ((int)v == 3));
level1.Level2.Level3.Property = 3; level1.Level2.Level3.Property = 3;
Assert.True(hit); Assert.True(received);
} }
} }
} }

29
Tests/Perspex.Markup.Xaml.UnitTests/DataContextChangeSynchronizerTest.cs

@ -27,8 +27,8 @@
[Fact] [Fact]
public void SameTypesFromUIToModel() public void SameTypesFromUIToModel()
{ {
var synchronizer = new DataContextChangeSynchronizer(guiObject, SamplePerspexObject.IntProperty, new PropertyPath("IntProp"), viewModel, repo); var synchronizer = new DataContextChangeSynchronizer(new DataContextChangeSynchronizer.BindingSource(new PropertyPath("IntProp"), viewModel), new DataContextChangeSynchronizer.BindingTarget(guiObject, SamplePerspexObject.IntProperty), repo);
synchronizer.SubscribeModelToUI(); synchronizer.StartUpdatingSourceWhenTargetChanges();
const int someValue = 4; const int someValue = 4;
guiObject.Int = someValue; guiObject.Int = someValue;
@ -39,8 +39,8 @@
[Fact] [Fact]
public void DifferentTypesFromUIToModel() public void DifferentTypesFromUIToModel()
{ {
var synchronizer = new DataContextChangeSynchronizer(guiObject, SamplePerspexObject.StringProperty, new PropertyPath("IntProp"), viewModel, repo); var synchronizer = new DataContextChangeSynchronizer(new DataContextChangeSynchronizer.BindingSource(new PropertyPath("IntProp"), viewModel), new DataContextChangeSynchronizer.BindingTarget(guiObject, SamplePerspexObject.StringProperty), repo);
synchronizer.SubscribeModelToUI(); synchronizer.StartUpdatingSourceWhenTargetChanges();
guiObject.String = "2"; guiObject.String = "2";
@ -50,8 +50,8 @@
[Fact] [Fact]
public void DifferentTypesAndNonConvertibleValueFromUIToModel() public void DifferentTypesAndNonConvertibleValueFromUIToModel()
{ {
var synchronizer = new DataContextChangeSynchronizer(guiObject, SamplePerspexObject.StringProperty, new PropertyPath("IntProp"), viewModel, repo); var synchronizer = new DataContextChangeSynchronizer(new DataContextChangeSynchronizer.BindingSource(new PropertyPath("IntProp"), viewModel), new DataContextChangeSynchronizer.BindingTarget(guiObject, SamplePerspexObject.StringProperty), repo);
synchronizer.SubscribeModelToUI(); synchronizer.StartUpdatingSourceWhenTargetChanges();
guiObject.String = ""; guiObject.String = "";
@ -62,8 +62,8 @@
[Fact] [Fact]
public void DifferentTypesFromModelToUI() public void DifferentTypesFromModelToUI()
{ {
var synchronizer = new DataContextChangeSynchronizer(guiObject, SamplePerspexObject.StringProperty, new PropertyPath("IntProp"), viewModel, repo); var synchronizer = new DataContextChangeSynchronizer(new DataContextChangeSynchronizer.BindingSource(new PropertyPath("IntProp"), viewModel), new DataContextChangeSynchronizer.BindingTarget(guiObject, SamplePerspexObject.StringProperty), repo);
synchronizer.SubscribeUIToModel(); synchronizer.StartUpdatingTargetWhenSourceChanges();
viewModel.IntProp = 2; viewModel.IntProp = 2;
@ -73,8 +73,8 @@
[Fact] [Fact]
public void SameTypesFromModelToUI() public void SameTypesFromModelToUI()
{ {
var synchronizer = new DataContextChangeSynchronizer(guiObject, SamplePerspexObject.IntProperty, new PropertyPath("IntProp"), viewModel, repo); var synchronizer = new DataContextChangeSynchronizer(new DataContextChangeSynchronizer.BindingSource(new PropertyPath("IntProp"), viewModel), new DataContextChangeSynchronizer.BindingTarget(guiObject, SamplePerspexObject.IntProperty), repo);
synchronizer.SubscribeUIToModel(); synchronizer.StartUpdatingTargetWhenSourceChanges();
viewModel.IntProp = 2; viewModel.IntProp = 2;
@ -87,14 +87,9 @@
var mainWindowViewModel = new MainWindowViewModel(); var mainWindowViewModel = new MainWindowViewModel();
var contentControl = new ContentControl(); var contentControl = new ContentControl();
var synchronizer = new DataContextChangeSynchronizer( var synchronizer = new DataContextChangeSynchronizer(new DataContextChangeSynchronizer.BindingSource(new PropertyPath("Content"), mainWindowViewModel), new DataContextChangeSynchronizer.BindingTarget(contentControl, ContentControl.ContentProperty), repo);
contentControl,
ContentControl.ContentProperty,
new PropertyPath("Content"),
mainWindowViewModel,
repo);
synchronizer.SubscribeUIToModel(); synchronizer.StartUpdatingTargetWhenSourceChanges();
var logInViewModel = new LogInViewModel(); var logInViewModel = new LogInViewModel();
mainWindowViewModel.Content = logInViewModel; mainWindowViewModel.Content = logInViewModel;

2
Tests/Perspex.Markup.Xaml.UnitTests/XamlBindingTest.cs

@ -12,7 +12,7 @@
{ {
var t = new Mock<ITypeConverterProvider>(); var t = new Mock<ITypeConverterProvider>();
var sut = new XamlBinding(t.Object); var sut = new XamlBinding(t.Object);
sut.Bind(null); sut.BindToDataContext(null);
} }
} }
} }

4
src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs

@ -56,7 +56,7 @@ namespace Perspex.Markup.Xaml.Context
private void HandlePerspexProperty(object instance, object value) private void HandlePerspexProperty(object instance, object value)
{ {
var pp = this.PerspexProperty; var pp = this.PerspexProperty;
var po = (PerspexObject) instance; var po = (PerspexObject)instance;
po.SetValue(pp, value); po.SetValue(pp, value);
} }
@ -75,7 +75,7 @@ namespace Perspex.Markup.Xaml.Context
var dataContext = target.DataContext; var dataContext = target.DataContext;
var binding = this.propertyBinder.GetBinding(target, definition.TargetProperty); var binding = this.propertyBinder.GetBinding(target, definition.TargetProperty);
binding.Bind(dataContext); binding.BindToDataContext(dataContext);
} }
// ReSharper disable once MemberCanBePrivate.Global // ReSharper disable once MemberCanBePrivate.Global

53
src/Markup/Perspex.Markup.Xaml/DataBinding/ChangeTracking/ObservablePropertyBranch.cs

@ -16,41 +16,46 @@ namespace Perspex.Markup.Xaml.DataBinding.ChangeTracking
public class ObservablePropertyBranch public class ObservablePropertyBranch
{ {
private readonly object root; private readonly object instance;
private readonly PropertyPath propertyPath; private readonly PropertyPath propertyPath;
private PropertyMountPoint mountPoint; private readonly PropertyMountPoint mountPoint;
public ObservablePropertyBranch(object root, PropertyPath propertyPath) public ObservablePropertyBranch(object instance, PropertyPath propertyPath)
{ {
Guard.ThrowIfNull(root, nameof(root)); Guard.ThrowIfNull(instance, nameof(instance));
Guard.ThrowIfNull(propertyPath, nameof(propertyPath)); Guard.ThrowIfNull(propertyPath, nameof(propertyPath));
this.root = root; this.instance = instance;
this.propertyPath = propertyPath; this.propertyPath = propertyPath;
this.mountPoint = new PropertyMountPoint(root, propertyPath); this.mountPoint = new PropertyMountPoint(instance, propertyPath);
var subscriptions = this.GetInpcNodes(); var properties = this.GetPropertiesThatRaiseNotifications();
this.Changed = this.CreateObservableFromNodes(subscriptions); this.Values = this.CreateUnifiedObservableFromNodes(properties);
} }
private IObservable<object> CreateObservableFromNodes(IEnumerable<InpcNode> subscriptions) public IObservable<object> Values { get; private set; }
private IObservable<object> CreateUnifiedObservableFromNodes(IEnumerable<PropertyDefinition> subscriptions)
{ {
return subscriptions.Select( return subscriptions.Select(this.GetObservableFromProperty).Merge();
subscription => Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(
ev => subscription.Parent.PropertyChanged += ev,
handler => subscription.Parent.PropertyChanged -= handler)
.Do(_ => this.mountPoint = new PropertyMountPoint(this.root, this.propertyPath))
.Where(pattern => pattern.EventArgs.PropertyName == subscription.PropertyName))
.Merge();
} }
private IEnumerable<InpcNode> GetInpcNodes() private IObservable<object> GetObservableFromProperty(PropertyDefinition subscription)
{ {
return this.GetSubscriptionsRecursive(this.root, this.propertyPath, 0); return Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(
parentOnPropertyChanged => subscription.Parent.PropertyChanged += parentOnPropertyChanged,
parentOnPropertyChanged => subscription.Parent.PropertyChanged -= parentOnPropertyChanged)
.Where(pattern => pattern.EventArgs.PropertyName == subscription.PropertyName)
.Select(pattern => this.mountPoint.Value);
} }
private IEnumerable<InpcNode> GetSubscriptionsRecursive(object current, PropertyPath propertyPath, int i) private IEnumerable<PropertyDefinition> GetPropertiesThatRaiseNotifications()
{ {
var subscriptions = new List<InpcNode>(); return this.GetSubscriptionsRecursive(this.instance, this.propertyPath, 0);
}
private IEnumerable<PropertyDefinition> GetSubscriptionsRecursive(object current, PropertyPath propertyPath, int i)
{
var subscriptions = new List<PropertyDefinition>();
var inpc = current as INotifyPropertyChanged; var inpc = current as INotifyPropertyChanged;
if (inpc == null) if (inpc == null)
@ -59,7 +64,7 @@ namespace Perspex.Markup.Xaml.DataBinding.ChangeTracking
} }
var nextPropertyName = propertyPath.Chunks[i]; var nextPropertyName = propertyPath.Chunks[i];
subscriptions.Add(new InpcNode(inpc, nextPropertyName)); subscriptions.Add(new PropertyDefinition(inpc, nextPropertyName));
if (i < this.propertyPath.Chunks.Length) if (i < this.propertyPath.Chunks.Length)
{ {
@ -76,8 +81,6 @@ namespace Perspex.Markup.Xaml.DataBinding.ChangeTracking
return subscriptions; return subscriptions;
} }
public IObservable<object> Changed { get; }
public object Value public object Value
{ {
get get
@ -93,9 +96,9 @@ namespace Perspex.Markup.Xaml.DataBinding.ChangeTracking
public Type Type => this.mountPoint.ProperyType; public Type Type => this.mountPoint.ProperyType;
private class InpcNode private class PropertyDefinition
{ {
public InpcNode(INotifyPropertyChanged parent, string propertyName) public PropertyDefinition(INotifyPropertyChanged parent, string propertyName)
{ {
this.Parent = parent; this.Parent = parent;
this.PropertyName = propertyName; this.PropertyName = propertyName;

171
src/Markup/Perspex.Markup.Xaml/DataBinding/DataContextChangeSynchronizer.cs

@ -7,8 +7,8 @@
namespace Perspex.Markup.Xaml.DataBinding namespace Perspex.Markup.Xaml.DataBinding
{ {
using System; using System;
using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.Reactive.Linq;
using System.Reflection; using System.Reflection;
using ChangeTracking; using ChangeTracking;
using Glass; using Glass;
@ -16,104 +16,153 @@ namespace Perspex.Markup.Xaml.DataBinding
public class DataContextChangeSynchronizer public class DataContextChangeSynchronizer
{ {
private readonly BindingTarget bindingTarget;
private readonly ITypeConverter targetPropertyTypeConverter; private readonly ITypeConverter targetPropertyTypeConverter;
private readonly TargetBindingEndpoint bindingEndpoint; private readonly TargetBindingEndpoint bindingEndpoint;
private readonly ObservablePropertyBranch sourceEndpoint; private readonly ObservablePropertyBranch sourceEndpoint;
public DataContextChangeSynchronizer(PerspexObject target, PerspexProperty targetProperty, public DataContextChangeSynchronizer(BindingSource bindingSource, BindingTarget bindingTarget, ITypeConverterProvider typeConverterProvider)
PropertyPath sourcePropertyPath, object source, ITypeConverterProvider typeConverterProvider)
{ {
Guard.ThrowIfNull(target, nameof(target)); this.bindingTarget = bindingTarget;
Guard.ThrowIfNull(targetProperty, nameof(targetProperty)); Guard.ThrowIfNull(bindingTarget.Object, nameof(bindingTarget.Object));
Guard.ThrowIfNull(sourcePropertyPath, nameof(sourcePropertyPath)); Guard.ThrowIfNull(bindingTarget.Property, nameof(bindingTarget.Property));
Guard.ThrowIfNull(source, nameof(source)); Guard.ThrowIfNull(bindingSource.SourcePropertyPath, nameof(bindingSource.SourcePropertyPath));
Guard.ThrowIfNull(bindingSource.Source, nameof(bindingSource.Source));
Guard.ThrowIfNull(typeConverterProvider, nameof(typeConverterProvider)); Guard.ThrowIfNull(typeConverterProvider, nameof(typeConverterProvider));
this.bindingEndpoint = new TargetBindingEndpoint(target, targetProperty); this.bindingEndpoint = new TargetBindingEndpoint(bindingTarget.Object, bindingTarget.Property);
this.sourceEndpoint = new ObservablePropertyBranch(source, sourcePropertyPath); this.sourceEndpoint = new ObservablePropertyBranch(bindingSource.Source, bindingSource.SourcePropertyPath);
this.targetPropertyTypeConverter = typeConverterProvider.GetTypeConverter(targetProperty.PropertyType); this.targetPropertyTypeConverter = typeConverterProvider.GetTypeConverter(bindingTarget.Property.PropertyType);
} }
private bool CanAssignWithoutConversion public class BindingTarget
{ {
get private readonly PerspexObject obj;
private readonly PerspexProperty property;
public BindingTarget(PerspexObject @object, PerspexProperty property)
{ {
var sourceTypeInfo = this.sourceEndpoint.Type.GetTypeInfo(); this.obj = @object;
var targetTypeInfo = this.bindingEndpoint.Property.PropertyType.GetTypeInfo(); this.property = property;
var compatible = targetTypeInfo.IsAssignableFrom(sourceTypeInfo); }
return compatible;
public PerspexObject Object => obj;
public PerspexProperty Property => property;
public object Value
{
get { return obj.GetValue(property); }
set { obj.SetValue(property, value); }
} }
} }
public void SubscribeModelToUI() public class BindingSource
{ {
this.bindingEndpoint.Object.GetObservable(this.bindingEndpoint.Property).Subscribe(this.UpdateModelFromUI); private readonly PropertyPath sourcePropertyPath;
private readonly object source;
public BindingSource(PropertyPath sourcePropertyPath, object source)
{
this.sourcePropertyPath = sourcePropertyPath;
this.source = source;
}
public PropertyPath SourcePropertyPath => this.sourcePropertyPath;
public object Source => source;
} }
public void SubscribeUIToModel() public void StartUpdatingTargetWhenSourceChanges()
{ {
this.sourceEndpoint.Changed.Subscribe(_ => this.UpdateUIFromModel()); // TODO: commenting out this line will make the existing value to be skipped from the SourceValues. This is not supposed to happen. Is it?
this.UpdateUIFromModel(); bindingTarget.Value = ConvertedValue(sourceEndpoint.Value, bindingTarget.Property.PropertyType);
// We use the native Bind method from PerspexObject to subscribe to the SourceValues observable
this.bindingTarget.Object.Bind(this.bindingTarget.Property, this.SourceValues);
} }
private void UpdateUIFromModel() public void StartUpdatingSourceWhenTargetChanges()
{ {
object contextGetter = this.sourceEndpoint.Value; // We subscribe to the TargetValues and each time we have a new value, we update the source with it
this.SetCompatibleValue(contextGetter, this.bindingEndpoint.Property.PropertyType, o => this.bindingEndpoint.Object.SetValue(this.bindingEndpoint.Property, o)); this.TargetValues.Subscribe(newValue => this.sourceEndpoint.Value = newValue);
} }
private void SetCompatibleValue(object originalValue, Type targetType, Action<object> setValueFunc) private IObservable<object> SourceValues
{ {
if (originalValue == null) get
{ {
setValueFunc(null); return this.sourceEndpoint.Values.Select(originalValue => this.ConvertedValue(originalValue, this.bindingTarget.Property.PropertyType));
} }
else }
private IObservable<object> TargetValues
{
get
{
return this.bindingEndpoint.Object
.GetObservable(this.bindingEndpoint.Property).Select(o => this.ConvertedValue(o, this.sourceEndpoint.Type));
}
}
private bool CanAssignWithoutConversion
{
get
{
var sourceTypeInfo = this.sourceEndpoint.Type.GetTypeInfo();
var targetTypeInfo = this.bindingEndpoint.Property.PropertyType.GetTypeInfo();
var compatible = targetTypeInfo.IsAssignableFrom(sourceTypeInfo);
return compatible;
}
}
private object ConvertedValue(object originalValue, Type propertyType)
{
object converted;
if (this.TryConvert(originalValue, propertyType, out converted))
{
return converted;
}
return null;
}
private bool TryConvert(object originalValue, Type targetType, out object finalValue)
{
if (originalValue != null)
{ {
if (this.CanAssignWithoutConversion) if (this.CanAssignWithoutConversion)
{ {
setValueFunc(originalValue); finalValue = originalValue;
return true;
} }
else
{
var synchronizationOk = false;
if (this.targetPropertyTypeConverter != null) if (this.targetPropertyTypeConverter != null)
{
if (this.targetPropertyTypeConverter.CanConvertTo(null, targetType))
{ {
if (this.targetPropertyTypeConverter.CanConvertTo(null, targetType)) object convertedValue = this.targetPropertyTypeConverter.ConvertTo(
null,
CultureInfo.InvariantCulture,
originalValue,
targetType);
if (convertedValue != null)
{ {
object convertedValue = this.targetPropertyTypeConverter.ConvertTo(null, CultureInfo.InvariantCulture, originalValue, finalValue = convertedValue;
targetType); return true;
if (convertedValue != null)
{
setValueFunc(convertedValue);
synchronizationOk = true;
}
} }
} }
if (!synchronizationOk)
{
this.LogCannotConvertError(originalValue);
}
} }
} }
} else
{
private void UpdateModelFromUI(object valueFromUI) finalValue = null;
{ return true;
this.SetCompatibleValue(valueFromUI, this.sourceEndpoint.Type, o => this.sourceEndpoint.Value = o); }
}
private void LogCannotConvertError(object value)
{
Contract.Requires<ArgumentException>(value != null);
var loggableValue = value.ToString();
var valueToWrite = string.IsNullOrWhiteSpace(loggableValue) ? "'(empty/whitespace string)'" : loggableValue;
Debug.WriteLine("Cannot convert value {0} ({1}) to {2}", valueToWrite, value.GetType(), this.bindingEndpoint.Property.PropertyType); finalValue = null;
return false;
} }
} }
} }

4
src/Markup/Perspex.Markup.Xaml/DataBinding/PerspexPropertyBinder.cs

@ -31,8 +31,8 @@ namespace Perspex.Markup.Xaml.DataBinding
public IEnumerable<XamlBinding> GetBindings(PerspexObject source) public IEnumerable<XamlBinding> GetBindings(PerspexObject source)
{ {
return from binding in this.bindings return from binding in this.bindings
where binding.Target == source where binding.Target == source
select binding; select binding;
} }
public XamlBinding Create(XamlBindingDefinition xamlBinding) public XamlBinding Create(XamlBindingDefinition xamlBinding)

41
src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBinding.cs

@ -14,6 +14,12 @@ namespace Perspex.Markup.Xaml.DataBinding
public class XamlBinding public class XamlBinding
{ {
private readonly ITypeConverterProvider typeConverterProvider; private readonly ITypeConverterProvider typeConverterProvider;
private DataContextChangeSynchronizer changeSynchronizer;
public XamlBinding(ITypeConverterProvider typeConverterProvider)
{
this.typeConverterProvider = typeConverterProvider;
}
public PerspexObject Target { get; set; } public PerspexObject Target { get; set; }
@ -23,12 +29,7 @@ namespace Perspex.Markup.Xaml.DataBinding
public BindingMode BindingMode { get; set; } public BindingMode BindingMode { get; set; }
public XamlBinding(ITypeConverterProvider typeConverterProvider) public void BindToDataContext(object dataContext)
{
this.typeConverterProvider = typeConverterProvider;
}
public void Bind(object dataContext)
{ {
if (dataContext == null) if (dataContext == null)
{ {
@ -37,35 +38,25 @@ namespace Perspex.Markup.Xaml.DataBinding
try try
{ {
var bindingSource = new DataContextChangeSynchronizer.BindingSource(this.SourcePropertyPath, dataContext);
var bindingTarget = new DataContextChangeSynchronizer.BindingTarget(this.Target, this.TargetProperty);
this.changeSynchronizer = new DataContextChangeSynchronizer(bindingSource, bindingTarget, this.typeConverterProvider);
if (this.BindingMode == BindingMode.TwoWay) if (this.BindingMode == BindingMode.TwoWay)
{ {
var changeSynchronizer = new DataContextChangeSynchronizer( this.changeSynchronizer.StartUpdatingTargetWhenSourceChanges();
this.Target, this.changeSynchronizer.StartUpdatingSourceWhenTargetChanges();
this.TargetProperty,
this.SourcePropertyPath,
dataContext,
this.typeConverterProvider);
changeSynchronizer.SubscribeUIToModel();
changeSynchronizer.SubscribeModelToUI();
} }
if (this.BindingMode == BindingMode.OneWay) if (this.BindingMode == BindingMode.OneWay)
{ {
var subscriptionHandler = new DataContextChangeSynchronizer( this.changeSynchronizer.StartUpdatingTargetWhenSourceChanges();
this.Target,
this.TargetProperty,
this.SourcePropertyPath,
dataContext,
this.typeConverterProvider);
subscriptionHandler.SubscribeUIToModel();
} }
if (this.BindingMode == BindingMode.OneWayToSource) if (this.BindingMode == BindingMode.OneWayToSource)
{ {
var subscriptionHandler = new DataContextChangeSynchronizer(this.Target, this.TargetProperty, this.SourcePropertyPath, dataContext, this.typeConverterProvider); this.changeSynchronizer.StartUpdatingSourceWhenTargetChanges();
subscriptionHandler.SubscribeModelToUI();
} }
} }
catch (Exception e) catch (Exception e)

Loading…
Cancel
Save