Browse Source

Started adding an ExpressionObserver plugin system.

So that we can handle PerspexObjects etc in an extensible fashion.
pull/297/head
Steven Kirk 11 years ago
parent
commit
06f59e57f4
  1. 12
      src/Markup/Perspex.Markup/Data/ExpressionObserver.cs
  2. 35
      src/Markup/Perspex.Markup/Data/Plugins/IPropertyAccessor.cs
  3. 33
      src/Markup/Perspex.Markup/Data/Plugins/IPropertyAccessorPlugin.cs
  4. 132
      src/Markup/Perspex.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs
  5. 61
      src/Markup/Perspex.Markup/Data/PropertyAccessorNode.cs
  6. 3
      src/Markup/Perspex.Markup/Perspex.Markup.csproj

12
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
/// </summary>
public class ExpressionObserver : ObservableBase<object>, IDescription
{
/// <summary>
/// An ordered collection of property accessor plugins that can be used to customize
/// the reading and subscription of property values on a type.
/// </summary>
public static readonly IList<IPropertyAccessorPlugin> PropertyAccessors =
new List<IPropertyAccessorPlugin>
{
new InpcPropertyAccessorPlugin()
};
private Func<object> _root;
private int _count;
private ExpressionNode _node;

35
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
{
/// <summary>
/// Defines an accessor to a property on an object returned by a
/// <see cref="IPropertyAccessorPlugin"/>
/// </summary>
public interface IPropertyAccessor : IDisposable
{
/// <summary>
/// Gets the type of the property.
/// </summary>
Type PropertyType { get; }
/// <summary>
/// Gets the current value of the property.
/// </summary>
object Value { get; }
/// <summary>
/// Sets the property value.
/// </summary>
/// <param name="value">
/// The value to set. Guaranteed to be of a valid type for the property.
/// </param>
/// <returns>
/// True if the property was set; false if the property could not be set.
/// </returns>
bool SetValue(object value);
}
}

33
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
{
/// <summary>
/// Defines how a member is read, written and observed by a
/// <see cref="ExpressionObserver"/>.
/// </summary>
public interface IPropertyAccessorPlugin
{
/// <summary>
/// Checks whether this plugin can handle accessing the properties of the specified object.
/// </summary>
/// <param name="instance">The object.</param>
/// <returns>True if the plugin can handle the object; otherwise false.</returns>
bool Match(object instance);
/// <summary>
/// Starts monitoring the value of a property on an object.
/// </summary>
/// <param name="instance">The object.</param>
/// <param name="propertyName">The property name.</param>
/// <param name="changed">A function to call when the property changes.</param>
/// <returns>
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
/// property will be made, or null if the property was not found.
/// </returns>
IPropertyAccessor Start(object instance, string propertyName, Action<object> changed);
}
}

132
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
{
/// <summary>
/// Reads a property from a standard C# object that optionally supports the
/// <see cref="INotifyPropertyChanged"/> interface.
/// </summary>
public class InpcPropertyAccessorPlugin : IPropertyAccessorPlugin
{
/// <summary>
/// Checks whether this plugin can handle accessing the properties of the specified object.
/// </summary>
/// <param name="instance">The object.</param>
/// <returns>True if the plugin can handle the object; otherwise false.</returns>
public bool Match(object instance)
{
Contract.Requires<ArgumentNullException>(instance != null);
return true;
}
/// <summary>
/// Starts monitoring the value of a property on an object.
/// </summary>
/// <param name="instance">The object.</param>
/// <param name="propertyName">The property name.</param>
/// <param name="changed">A function to call when the property changes.</param>
/// <returns>
/// An <see cref="IPropertyAccessor"/> interface through which future interactions with the
/// property will be made, or null if the property was not found.
/// </returns>
public IPropertyAccessor Start(object instance, string propertyName, Action<object> changed)
{
Contract.Requires<ArgumentNullException>(instance != null);
Contract.Requires<ArgumentNullException>(propertyName != null);
Contract.Requires<ArgumentNullException>(changed != null);
var p = instance.GetType().GetRuntimeProperty(propertyName);
if (p != null)
{
return new Accessor(instance, p, changed);
}
else
{
return null;
}
}
private IObservable<object> GetObservable(
INotifyPropertyChanged inpc,
PropertyInfo property,
object o)
{
return Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(
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<object> _changed;
public Accessor(object instance, PropertyInfo property, Action<object> changed)
{
Contract.Requires<ArgumentNullException>(instance != null);
Contract.Requires<ArgumentNullException>(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);
}
}
}
}
}

61
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<object>;
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);
}
}
}
}

3
src/Markup/Perspex.Markup/Perspex.Markup.csproj

@ -42,6 +42,9 @@
<Compile Include="Data\ExpressionNodeBuilder.cs" />
<Compile Include="Data\ExpressionParseException.cs" />
<Compile Include="Data\ExpressionSubject.cs" />
<Compile Include="Data\Plugins\InpcPropertyAccessorPlugin.cs" />
<Compile Include="Data\Plugins\IPropertyAccessor.cs" />
<Compile Include="Data\Plugins\IPropertyAccessorPlugin.cs" />
<Compile Include="Data\LogicalNotNode.cs" />
<Compile Include="Data\IndexerNode.cs" />
<Compile Include="Data\Parsers\ArgumentListParser.cs" />

Loading…
Cancel
Save