// 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.Collections.Generic;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Perspex.Data;
using Perspex.Markup.Data.Plugins;
namespace Perspex.Markup.Data
{
///
/// Observes and sets the value of an expression on an object.
///
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 PerspexPropertyAccessorPlugin(),
new InpcPropertyAccessorPlugin(),
};
///
/// An ordered collection of validation checker plugins that can be used to customize
/// the validation of view model and model data.
///
public static readonly IList ValidationCheckers =
new List
{
new IndeiValidationCheckerPlugin(),
new ExceptionValidationCheckerPlugin()
};
private readonly WeakReference _root;
private readonly Func _rootGetter;
private readonly IObservable _rootObservable;
private readonly IObservable _update;
private IDisposable _rootObserverSubscription;
private IDisposable _updateSubscription;
private int _count;
private readonly ExpressionNode _node;
private ValidationMethods _methods;
///
/// Initializes a new instance of the class.
///
/// The root object.
/// The expression.
/// The validation methods to enable on this observer.
public ExpressionObserver(object root, string expression, ValidationMethods methods = ValidationMethods.None)
{
Contract.Requires(expression != null);
_root = new WeakReference(root);
_methods = methods;
if (!string.IsNullOrWhiteSpace(expression))
{
_node = ExpressionNodeBuilder.Build(expression);
}
Expression = expression;
}
///
/// Initializes a new instance of the class.
///
/// An observable which provides the root object.
/// The expression.
/// The validation methods to enable on this observer.
public ExpressionObserver(IObservable rootObservable, string expression, ValidationMethods methods = ValidationMethods.None)
{
Contract.Requires(rootObservable != null);
Contract.Requires(expression != null);
_rootObservable = rootObservable;
_methods = methods;
if (!string.IsNullOrWhiteSpace(expression))
{
_node = ExpressionNodeBuilder.Build(expression);
}
Expression = expression;
}
///
/// Initializes a new instance of the class.
///
/// A function which gets the root object.
/// The expression.
/// An observable which triggers a re-read of the getter.
/// The validation methods to enable on this observer.
public ExpressionObserver(
Func rootGetter,
string expression,
IObservable update,
ValidationMethods methods = ValidationMethods.None)
{
Contract.Requires(rootGetter != null);
Contract.Requires(expression != null);
Contract.Requires(update != null);
_rootGetter = rootGetter;
_update = update;
_methods = methods;
if (!string.IsNullOrWhiteSpace(expression))
{
_node = ExpressionNodeBuilder.Build(expression);
}
Expression = expression;
}
///
/// Attempts to set the value of a property expression.
///
/// The value to set.
/// The binding priority to use.
///
/// True if the value could be set; false if the expression does not evaluate to a
/// property.
///
public bool SetValue(object value, BindingPriority priority = BindingPriority.LocalValue)
{
IncrementCount();
if (_rootGetter != null && _node != null)
{
_node.Target = new WeakReference(_rootGetter());
}
try
{
return _node?.SetValue(value, priority) ?? false;
}
finally
{
DecrementCount();
}
}
///
/// Gets the expression being observed.
///
public string Expression { get; }
///
/// Gets the type of the expression result or null if the expression could not be
/// evaluated.
///
public Type ResultType
{
get
{
IncrementCount();
try
{
if (_node != null)
{
return (Leaf as PropertyAccessorNode)?.PropertyType;
}
else if(_rootGetter != null)
{
return _rootGetter()?.GetType();
}
else
{
return _root.Target?.GetType();
}
}
finally
{
DecrementCount();
}
}
}
///
string IDescription.Description => Expression;
///
/// Gets the root expression node. Used for testing.
///
internal ExpressionNode Node => _node;
///
/// Gets the leaf node.
///
private ExpressionNode Leaf
{
get
{
var node = _node;
while (node.Next != null) node = node.Next;
return node;
}
}
///
protected override IDisposable SubscribeCore(IObserver observer)
{
IncrementCount();
if (_node != null)
{
IObservable source = _node;
if (_rootObservable != null)
{
source = source.TakeUntil(_rootObservable.LastOrDefaultAsync());
}
else if (_update != null)
{
source = source.TakeUntil(_update.LastOrDefaultAsync());
}
var validationFiltered = source.Where(o => (o as ValidationStatus)?.Match(_methods) ?? true);
var subscription = validationFiltered.Subscribe(observer);
return Disposable.Create(() =>
{
DecrementCount();
subscription.Dispose();
});
}
else if (_rootObservable != null)
{
return _rootObservable.Subscribe(observer);
}
else
{
if (_update == null)
{
return Observable.Never()
.StartWith(_root.Target)
.Subscribe(observer);
}
else
{
return _update
.Select(_ => _rootGetter())
.StartWith(_rootGetter())
.Subscribe(observer);
}
}
}
private void IncrementCount()
{
if (_count++ == 0 && _node != null)
{
if (_rootGetter != null)
{
_node.Target = new WeakReference(_rootGetter());
if (_update != null)
{
_updateSubscription = _update.Subscribe(x =>
_node.Target = new WeakReference(_rootGetter()));
}
}
else if (_rootObservable != null)
{
_rootObserverSubscription = _rootObservable.Subscribe(x =>
_node.Target = new WeakReference(x));
}
else
{
_node.Target = _root;
}
}
}
private void DecrementCount()
{
if (--_count == 0 && _node != null)
{
if (_rootObserverSubscription != null)
{
_rootObserverSubscription.Dispose();
_rootObserverSubscription = null;
}
if (_updateSubscription != null)
{
_updateSubscription.Dispose();
_updateSubscription = null;
}
_node.Target = null;
}
}
}
}