diff --git a/src/Markup/Perspex.Markup/Data/ExpressionObserver.cs b/src/Markup/Perspex.Markup/Data/ExpressionObserver.cs index 470e4d6248..df210eec73 100644 --- a/src/Markup/Perspex.Markup/Data/ExpressionObserver.cs +++ b/src/Markup/Perspex.Markup/Data/ExpressionObserver.cs @@ -2,8 +2,10 @@ // 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.Reactive; using System.Reactive.Disposables; +using Perspex.Markup.Data.Plugins; namespace Perspex.Markup.Data { @@ -12,6 +14,16 @@ namespace Perspex.Markup.Data /// public class ExpressionObserver : ObservableBase, IDescription { + /// + /// An ordered collection of property accessor plugins that can be used to customize + /// the reading and subscription of property values on a type. + /// + public static readonly IList PropertyAccessors = + new List + { + new InpcPropertyAccessorPlugin() + }; + private Func _root; private int _count; private ExpressionNode _node; diff --git a/src/Markup/Perspex.Markup/Data/Plugins/IPropertyAccessor.cs b/src/Markup/Perspex.Markup/Data/Plugins/IPropertyAccessor.cs new file mode 100644 index 0000000000..772bbbafe9 --- /dev/null +++ b/src/Markup/Perspex.Markup/Data/Plugins/IPropertyAccessor.cs @@ -0,0 +1,35 @@ +// 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; + +namespace Perspex.Markup.Data.Plugins +{ + /// + /// Defines an accessor to a property on an object returned by a + /// + /// + public interface IPropertyAccessor : IDisposable + { + /// + /// Gets the type of the property. + /// + Type PropertyType { get; } + + /// + /// Gets the current value of the property. + /// + object Value { get; } + + /// + /// Sets the property value. + /// + /// + /// The value to set. Guaranteed to be of a valid type for the property. + /// + /// + /// True if the property was set; false if the property could not be set. + /// + bool SetValue(object value); + } +} diff --git a/src/Markup/Perspex.Markup/Data/Plugins/IPropertyAccessorPlugin.cs b/src/Markup/Perspex.Markup/Data/Plugins/IPropertyAccessorPlugin.cs new file mode 100644 index 0000000000..012d28eaed --- /dev/null +++ b/src/Markup/Perspex.Markup/Data/Plugins/IPropertyAccessorPlugin.cs @@ -0,0 +1,33 @@ +// 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; + +namespace Perspex.Markup.Data.Plugins +{ + /// + /// Defines how a member is read, written and observed by a + /// . + /// + public interface IPropertyAccessorPlugin + { + /// + /// Checks whether this plugin can handle accessing the properties of the specified object. + /// + /// The object. + /// True if the plugin can handle the object; otherwise false. + bool Match(object instance); + + /// + /// Starts monitoring the value of a property on an object. + /// + /// The object. + /// The property name. + /// A function to call when the property changes. + /// + /// An interface through which future interactions with the + /// property will be made, or null if the property was not found. + /// + IPropertyAccessor Start(object instance, string propertyName, Action changed); + } +} diff --git a/src/Markup/Perspex.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs b/src/Markup/Perspex.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs new file mode 100644 index 0000000000..ea0659c10b --- /dev/null +++ b/src/Markup/Perspex.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs @@ -0,0 +1,132 @@ +// 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.ComponentModel; +using System.Reactive.Linq; +using System.Reflection; + +namespace Perspex.Markup.Data.Plugins +{ + /// + /// Reads a property from a standard C# object that optionally supports the + /// interface. + /// + public class InpcPropertyAccessorPlugin : IPropertyAccessorPlugin + { + /// + /// Checks whether this plugin can handle accessing the properties of the specified object. + /// + /// The object. + /// True if the plugin can handle the object; otherwise false. + public bool Match(object instance) + { + Contract.Requires(instance != null); + + return true; + } + + /// + /// Starts monitoring the value of a property on an object. + /// + /// The object. + /// The property name. + /// A function to call when the property changes. + /// + /// An interface through which future interactions with the + /// property will be made, or null if the property was not found. + /// + public IPropertyAccessor Start(object instance, string propertyName, Action changed) + { + Contract.Requires(instance != null); + Contract.Requires(propertyName != null); + Contract.Requires(changed != null); + + var p = instance.GetType().GetRuntimeProperty(propertyName); + + if (p != null) + { + return new Accessor(instance, p, changed); + } + else + { + return null; + } + } + + private IObservable GetObservable( + INotifyPropertyChanged inpc, + PropertyInfo property, + object o) + { + return Observable.FromEventPattern( + x => inpc.PropertyChanged += x, + x => inpc.PropertyChanged -= x) + .Where(e => e.EventArgs.PropertyName == property.Name) + .Select(_ => property.GetValue(o)); + } + + private class Accessor : IPropertyAccessor + { + private object _instance; + private PropertyInfo _property; + private Action _changed; + + public Accessor(object instance, PropertyInfo property, Action changed) + { + Contract.Requires(instance != null); + Contract.Requires(property != null); + + _instance = instance; + _property = property; + _changed = changed; + + var inpc = instance as INotifyPropertyChanged; + + if (inpc != null) + { + inpc.PropertyChanged += PropertyChanged; + } + } + + public Type PropertyType + { + get { return _property.PropertyType; } + } + + public object Value + { + get { return _property.GetValue(_instance); } + } + + public void Dispose() + { + var inpc = _instance as INotifyPropertyChanged; + + if (inpc != null) + { + inpc.PropertyChanged -= PropertyChanged; + } + } + + public bool SetValue(object value) + { + if (_property.CanWrite) + { + _property.SetValue(_instance, value); + return true; + } + + return false; + } + + private void PropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == _property.Name) + { + _changed(Value); + } + } + } + } +} diff --git a/src/Markup/Perspex.Markup/Data/PropertyAccessorNode.cs b/src/Markup/Perspex.Markup/Data/PropertyAccessorNode.cs index f4f2674d42..546b38ab0b 100644 --- a/src/Markup/Perspex.Markup/Data/PropertyAccessorNode.cs +++ b/src/Markup/Perspex.Markup/Data/PropertyAccessorNode.cs @@ -2,18 +2,19 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.ComponentModel; +using System.Linq; using System.Reactive.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; using System.Windows.Input; +using Perspex.Markup.Data.Plugins; namespace Perspex.Markup.Data { internal class PropertyAccessorNode : ExpressionNode { - private PropertyInfo _propertyInfo; + private IPropertyAccessor _accessor; private IDisposable _subscription; public PropertyAccessorNode(string propertyName) @@ -23,7 +24,7 @@ namespace Perspex.Markup.Data public string PropertyName { get; } - public Type PropertyType => _propertyInfo?.PropertyType; + public Type PropertyType => _accessor?.PropertyType; public override bool SetValue(object value) { @@ -33,10 +34,9 @@ namespace Perspex.Markup.Data } else { - if (_propertyInfo != null && _propertyInfo.CanWrite) + if (_accessor != null) { - _propertyInfo.SetValue(Target, value); - return true; + return _accessor.SetValue(value); } return false; @@ -45,57 +45,40 @@ namespace Perspex.Markup.Data protected override void SubscribeAndUpdate(object target) { - bool set = false; - if (target != null) { - _propertyInfo = target.GetType().GetRuntimeProperty(PropertyName); + var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(target)); - if (_propertyInfo != null) + if (plugin != null) { - ReadValue(target); - set = true; - - var inpc = target as INotifyPropertyChanged; + _accessor = plugin.Start(target, PropertyName, SetCurrentValue); - if (inpc != null) + if (_accessor != null) { - inpc.PropertyChanged += PropertyChanged; + SetCurrentValue(_accessor.Value); + return; } } } - else - { - _propertyInfo = null; - } - if (!set) - { - CurrentValue = PerspexProperty.UnsetValue; - } + CurrentValue = PerspexProperty.UnsetValue; } protected override void Unsubscribe(object target) { - var inpc = target as INotifyPropertyChanged; - - if (inpc != null) - { - inpc.PropertyChanged -= PropertyChanged; - } - - _propertyInfo = null; + _accessor?.Dispose(); + _accessor = null; } - private void ReadValue(object target) + private void SetCurrentValue(object value) { - var value = _propertyInfo.GetValue(target); var observable = value as IObservable; var command = value as ICommand; var task = value as Task; bool set = false; - // ReactiveCommand is an IObservable but we want to bind to it, not its value. + // HACK: ReactiveCommand is an IObservable but we want to bind to it, not its value. + // We may need to make this a more general solution. if (observable != null && command == null) { CurrentValue = PerspexProperty.UnsetValue; @@ -135,13 +118,5 @@ namespace Perspex.Markup.Data CurrentValue = PerspexProperty.UnsetValue; } } - - private void PropertyChanged(object sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == PropertyName) - { - ReadValue(sender); - } - } } } diff --git a/src/Markup/Perspex.Markup/Perspex.Markup.csproj b/src/Markup/Perspex.Markup/Perspex.Markup.csproj index de0d8e6ce3..eea1576051 100644 --- a/src/Markup/Perspex.Markup/Perspex.Markup.csproj +++ b/src/Markup/Perspex.Markup/Perspex.Markup.csproj @@ -42,6 +42,9 @@ + + +