|
|
|
@ -7,8 +7,8 @@ |
|
|
|
namespace Perspex.Markup.Xaml.DataBinding |
|
|
|
{ |
|
|
|
using System; |
|
|
|
using System.Diagnostics; |
|
|
|
using System.Globalization; |
|
|
|
using System.Reactive.Linq; |
|
|
|
using System.Reflection; |
|
|
|
using ChangeTracking; |
|
|
|
using Glass; |
|
|
|
@ -16,104 +16,153 @@ namespace Perspex.Markup.Xaml.DataBinding |
|
|
|
|
|
|
|
public class DataContextChangeSynchronizer |
|
|
|
{ |
|
|
|
private readonly BindingTarget bindingTarget; |
|
|
|
private readonly ITypeConverter targetPropertyTypeConverter; |
|
|
|
private readonly TargetBindingEndpoint bindingEndpoint; |
|
|
|
private readonly ObservablePropertyBranch sourceEndpoint; |
|
|
|
|
|
|
|
public DataContextChangeSynchronizer(PerspexObject target, PerspexProperty targetProperty, |
|
|
|
PropertyPath sourcePropertyPath, object source, ITypeConverterProvider typeConverterProvider) |
|
|
|
public DataContextChangeSynchronizer(BindingSource bindingSource, BindingTarget bindingTarget, ITypeConverterProvider typeConverterProvider) |
|
|
|
{ |
|
|
|
Guard.ThrowIfNull(target, nameof(target)); |
|
|
|
Guard.ThrowIfNull(targetProperty, nameof(targetProperty)); |
|
|
|
Guard.ThrowIfNull(sourcePropertyPath, nameof(sourcePropertyPath)); |
|
|
|
Guard.ThrowIfNull(source, nameof(source)); |
|
|
|
this.bindingTarget = bindingTarget; |
|
|
|
Guard.ThrowIfNull(bindingTarget.Object, nameof(bindingTarget.Object)); |
|
|
|
Guard.ThrowIfNull(bindingTarget.Property, nameof(bindingTarget.Property)); |
|
|
|
Guard.ThrowIfNull(bindingSource.SourcePropertyPath, nameof(bindingSource.SourcePropertyPath)); |
|
|
|
Guard.ThrowIfNull(bindingSource.Source, nameof(bindingSource.Source)); |
|
|
|
Guard.ThrowIfNull(typeConverterProvider, nameof(typeConverterProvider)); |
|
|
|
|
|
|
|
this.bindingEndpoint = new TargetBindingEndpoint(target, targetProperty); |
|
|
|
this.sourceEndpoint = new ObservablePropertyBranch(source, sourcePropertyPath); |
|
|
|
this.targetPropertyTypeConverter = typeConverterProvider.GetTypeConverter(targetProperty.PropertyType); |
|
|
|
this.bindingEndpoint = new TargetBindingEndpoint(bindingTarget.Object, bindingTarget.Property); |
|
|
|
this.sourceEndpoint = new ObservablePropertyBranch(bindingSource.Source, bindingSource.SourcePropertyPath); |
|
|
|
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(); |
|
|
|
var targetTypeInfo = this.bindingEndpoint.Property.PropertyType.GetTypeInfo(); |
|
|
|
var compatible = targetTypeInfo.IsAssignableFrom(sourceTypeInfo); |
|
|
|
return compatible; |
|
|
|
this.obj = @object; |
|
|
|
this.property = property; |
|
|
|
} |
|
|
|
|
|
|
|
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()); |
|
|
|
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?
|
|
|
|
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; |
|
|
|
this.SetCompatibleValue(contextGetter, this.bindingEndpoint.Property.PropertyType, o => this.bindingEndpoint.Object.SetValue(this.bindingEndpoint.Property, o)); |
|
|
|
// We subscribe to the TargetValues and each time we have a new value, we update the source with it
|
|
|
|
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) |
|
|
|
{ |
|
|
|
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, |
|
|
|
targetType); |
|
|
|
|
|
|
|
if (convertedValue != null) |
|
|
|
{ |
|
|
|
setValueFunc(convertedValue); |
|
|
|
synchronizationOk = true; |
|
|
|
} |
|
|
|
finalValue = convertedValue; |
|
|
|
return true; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (!synchronizationOk) |
|
|
|
{ |
|
|
|
this.LogCannotConvertError(originalValue); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private void UpdateModelFromUI(object valueFromUI) |
|
|
|
{ |
|
|
|
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; |
|
|
|
else |
|
|
|
{ |
|
|
|
finalValue = null; |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
Debug.WriteLine("Cannot convert value {0} ({1}) to {2}", valueToWrite, value.GetType(), this.bindingEndpoint.Property.PropertyType); |
|
|
|
finalValue = null; |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
} |