diff --git a/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs
index c32a08cb5d..1e996b2b2a 100644
--- a/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs
+++ b/src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs
@@ -25,6 +25,7 @@ namespace Perspex.Markup.Xaml.MarkupExtensions
Converter = Converter,
ConverterParameter = ConverterParameter,
ElementName = ElementName,
+ FallbackValue = FallbackValue,
Mode = Mode,
Path = Path,
Priority = Priority,
diff --git a/src/Markup/Perspex.Markup/Data/ExpressionSubject.cs b/src/Markup/Perspex.Markup/Data/ExpressionSubject.cs
index 95b8c7ac4d..46df3ce490 100644
--- a/src/Markup/Perspex.Markup/Data/ExpressionSubject.cs
+++ b/src/Markup/Perspex.Markup/Data/ExpressionSubject.cs
@@ -6,6 +6,7 @@ using System.Globalization;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Perspex.Data;
+using Perspex.Logging;
using Perspex.Utilities;
namespace Perspex.Markup.Data
@@ -103,9 +104,44 @@ namespace Perspex.Markup.Data
if (converted == PerspexProperty.UnsetValue)
{
converted = TypeUtilities.Default(type);
+ _inner.SetValue(converted, _priority);
+ }
+ else if (converted is BindingError)
+ {
+ var error = converted as BindingError;
+
+ Logger.Error(
+ LogArea.Binding,
+ this,
+ "Error binding to {Expression}: {Message}",
+ _inner.Expression,
+ error.Exception.Message);
+
+ if (_fallbackValue != null)
+ {
+ if (TypeUtilities.TryConvert(
+ type,
+ _fallbackValue,
+ CultureInfo.InvariantCulture,
+ out converted))
+ {
+ _inner.SetValue(converted, _priority);
+ }
+ else
+ {
+ Logger.Error(
+ LogArea.Binding,
+ this,
+ "Could not convert FallbackValue {FallbackValue} to {Type}",
+ _fallbackValue,
+ type);
+ }
+ }
+ }
+ else
+ {
+ _inner.SetValue(converted, _priority);
}
-
- _inner.SetValue(converted, _priority);
}
}
@@ -117,15 +153,37 @@ namespace Perspex.Markup.Data
private object ConvertValue(object value)
{
- var converted = Converter.Convert(
+ var converted =
+ value as BindingError ??
+ Converter.Convert(
value,
_targetType,
ConverterParameter,
CultureInfo.CurrentUICulture);
- if (converted == PerspexProperty.UnsetValue && _fallbackValue != null)
+ if (_fallbackValue != null &&
+ (converted == PerspexProperty.UnsetValue ||
+ converted is BindingError))
{
- converted = _fallbackValue;
+ var error = converted as BindingError;
+
+ if (TypeUtilities.TryConvert(
+ _targetType,
+ _fallbackValue,
+ CultureInfo.InvariantCulture,
+ out converted))
+ {
+ if (error != null)
+ {
+ converted = new BindingError(error.Exception, converted);
+ }
+ }
+ else
+ {
+ converted = new BindingError(
+ new InvalidCastException(
+ $"Could not convert FallbackValue '{_fallbackValue}' to '{_targetType}'"));
+ }
}
return converted;
diff --git a/src/Markup/Perspex.Markup/Data/Plugins/IPropertyAccessorPlugin.cs b/src/Markup/Perspex.Markup/Data/Plugins/IPropertyAccessorPlugin.cs
index 7bbdb0ff68..8a431041d0 100644
--- a/src/Markup/Perspex.Markup/Data/Plugins/IPropertyAccessorPlugin.cs
+++ b/src/Markup/Perspex.Markup/Data/Plugins/IPropertyAccessorPlugin.cs
@@ -26,7 +26,7 @@ namespace Perspex.Markup.Data.Plugins
/// 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.
+ /// property will be made.
///
IPropertyAccessor Start(
WeakReference reference,
diff --git a/src/Markup/Perspex.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs b/src/Markup/Perspex.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs
index ac38ee74f9..64421ddc57 100644
--- a/src/Markup/Perspex.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs
+++ b/src/Markup/Perspex.Markup/Data/Plugins/InpcPropertyAccessorPlugin.cs
@@ -38,7 +38,7 @@ namespace Perspex.Markup.Data.Plugins
/// 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.
+ /// property will be made.
///
public IPropertyAccessor Start(
WeakReference reference,
@@ -58,14 +58,9 @@ namespace Perspex.Markup.Data.Plugins
}
else
{
- Logger.Error(
- LogArea.Binding,
- this,
- "Could not find CLR property {Property} on {Source}",
- propertyName,
- instance);
-
- return null;
+ var message = $"Could not find CLR property '{propertyName}' on '{instance}'";
+ var exception = new MissingMemberException(message);
+ return new PropertyError(new BindingError(exception));
}
}
diff --git a/src/Markup/Perspex.Markup/Data/Plugins/PerspexPropertyAccessorPlugin.cs b/src/Markup/Perspex.Markup/Data/Plugins/PerspexPropertyAccessorPlugin.cs
index 4d2f6c5cb0..bc16989a7a 100644
--- a/src/Markup/Perspex.Markup/Data/Plugins/PerspexPropertyAccessorPlugin.cs
+++ b/src/Markup/Perspex.Markup/Data/Plugins/PerspexPropertyAccessorPlugin.cs
@@ -33,7 +33,7 @@ namespace Perspex.Markup.Data.Plugins
/// 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.
+ /// property will be made.
///
public IPropertyAccessor Start(
WeakReference reference,
@@ -52,14 +52,14 @@ namespace Perspex.Markup.Data.Plugins
{
return new Accessor(new WeakReference(o), p, changed);
}
+ else if (instance != PerspexProperty.UnsetValue)
+ {
+ var message = $"Could not find PerspexProperty '{propertyName}' on '{instance}'";
+ var exception = new MissingMemberException(message);
+ return new PropertyError(new BindingError(exception));
+ }
else
{
- Logger.Error(
- LogArea.Binding,
- this,
- "Could not find PerspexProperty {Property} on {Source}",
- propertyName,
- instance);
return null;
}
}
diff --git a/src/Markup/Perspex.Markup/Data/Plugins/PropertyError.cs b/src/Markup/Perspex.Markup/Data/Plugins/PropertyError.cs
new file mode 100644
index 0000000000..c147a315d5
--- /dev/null
+++ b/src/Markup/Perspex.Markup/Data/Plugins/PropertyError.cs
@@ -0,0 +1,39 @@
+using System;
+using Perspex.Data;
+
+namespace Perspex.Markup.Data.Plugins
+{
+ ///
+ /// An that represents an error.
+ ///
+ public class PropertyError : IPropertyAccessor
+ {
+ private BindingError _error;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The error to report.
+ public PropertyError(BindingError error)
+ {
+ _error = error;
+ }
+
+ ///
+ public Type PropertyType => null;
+
+ ///
+ public object Value => _error;
+
+ ///
+ public void Dispose()
+ {
+ }
+
+ ///
+ public bool SetValue(object value, BindingPriority priority)
+ {
+ return false;
+ }
+ }
+}
diff --git a/src/Markup/Perspex.Markup/Data/PropertyAccessorNode.cs b/src/Markup/Perspex.Markup/Data/PropertyAccessorNode.cs
index 1aa703ff8f..14883e449a 100644
--- a/src/Markup/Perspex.Markup/Data/PropertyAccessorNode.cs
+++ b/src/Markup/Perspex.Markup/Data/PropertyAccessorNode.cs
@@ -48,7 +48,7 @@ namespace Perspex.Markup.Data
{
var instance = reference.Target;
- if (instance != null)
+ if (instance != null && instance != PerspexProperty.UnsetValue)
{
var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference));
diff --git a/src/Markup/Perspex.Markup/DefaultValueConverter.cs b/src/Markup/Perspex.Markup/DefaultValueConverter.cs
index 9f535081a7..0cc5f0df8e 100644
--- a/src/Markup/Perspex.Markup/DefaultValueConverter.cs
+++ b/src/Markup/Perspex.Markup/DefaultValueConverter.cs
@@ -5,6 +5,7 @@ using System;
using System.Globalization;
using System.Linq;
using System.Reflection;
+using Perspex.Data;
using Perspex.Logging;
using Perspex.Utilities;
@@ -42,12 +43,8 @@ namespace Perspex.Markup
if (value != null)
{
- Logger.Error(
- LogArea.Binding,
- this,
- "Could not convert {Value} to {Type}",
- value,
- targetType);
+ var message = $"Could not convert '{value}' to '{targetType}'";
+ return new BindingError(new InvalidCastException(message));
}
return PerspexProperty.UnsetValue;
diff --git a/src/Markup/Perspex.Markup/Perspex.Markup.csproj b/src/Markup/Perspex.Markup/Perspex.Markup.csproj
index f5b61e2cea..5427d97346 100644
--- a/src/Markup/Perspex.Markup/Perspex.Markup.csproj
+++ b/src/Markup/Perspex.Markup/Perspex.Markup.csproj
@@ -56,6 +56,7 @@
+
diff --git a/src/Perspex.Base/Data/BindingError.cs b/src/Perspex.Base/Data/BindingError.cs
new file mode 100644
index 0000000000..b9330bd278
--- /dev/null
+++ b/src/Perspex.Base/Data/BindingError.cs
@@ -0,0 +1,59 @@
+// 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.Data
+{
+ ///
+ /// Represents a recoverable binding error.
+ ///
+ ///
+ /// When produced by a binding source observable, informs the binding system that an error
+ /// occurred. It can also provide an optional fallback value to be pushed to the binding
+ /// target.
+ ///
+ /// Instead of using , one could simply not push a value (in the
+ /// case of a no fallback value) or push a fallback value, but BindingError also causes an
+ /// error to be logged with the correct binding target.
+ ///
+ public class BindingError
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// An exception describing the binding error.
+ public BindingError(Exception exception)
+ {
+ Exception = exception;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// An exception describing the binding error.
+ /// The fallback value.
+ public BindingError(Exception exception, object fallbackValue)
+ {
+ Exception = exception;
+ FallbackValue = fallbackValue;
+ UseFallbackValue = true;
+ }
+
+ ///
+ /// Gets the exception describing the binding error.
+ ///
+ public Exception Exception { get; }
+
+ ///
+ /// Get the fallback value.
+ ///
+ public object FallbackValue { get; }
+
+ ///
+ /// Get a value indicating whether the fallback value should be pushed to the binding
+ /// target.
+ ///
+ public bool UseFallbackValue { get; }
+ }
+}
diff --git a/src/Perspex.Base/DirectProperty.cs b/src/Perspex.Base/DirectProperty.cs
index 7ae19ec520..82d47e7c1d 100644
--- a/src/Perspex.Base/DirectProperty.cs
+++ b/src/Perspex.Base/DirectProperty.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
+using Perspex.Data;
namespace Perspex
{
@@ -44,11 +45,13 @@ namespace Perspex
/// The property to copy.
/// Gets the current value of the property.
/// Sets the value of the property. May be null.
+ /// Optional overridden metadata.
private DirectProperty(
PerspexProperty source,
Func getter,
- Action setter)
- : base(source, typeof(TOwner))
+ Action setter,
+ PropertyMetadata metadata)
+ : base(source, typeof(TOwner), metadata)
{
Contract.Requires(getter != null);
@@ -76,16 +79,25 @@ namespace Perspex
/// Registers the direct property on another type.
///
/// The type of the additional owner.
+ /// Gets the current value of the property.
+ /// Sets the value of the property.
+ ///
+ /// The value to use when the property is set to
+ ///
+ /// The default binding mode for the property.
/// The property.
public DirectProperty AddOwner(
Func getter,
- Action setter = null)
+ Action setter = null,
+ TValue unsetValue = default(TValue),
+ BindingMode defaultBindingMode = BindingMode.OneWay)
where TNewOwner : PerspexObject
{
var result = new DirectProperty(
this,
getter,
- setter);
+ setter,
+ new DirectPropertyMetadata(unsetValue, defaultBindingMode));
PerspexPropertyRegistry.Instance.Register(typeof(TNewOwner), result);
return result;
diff --git a/src/Perspex.Base/IPriorityValueOwner.cs b/src/Perspex.Base/IPriorityValueOwner.cs
new file mode 100644
index 0000000000..aa79864794
--- /dev/null
+++ b/src/Perspex.Base/IPriorityValueOwner.cs
@@ -0,0 +1,19 @@
+// 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.
+
+namespace Perspex
+{
+ ///
+ /// An owner of a .
+ ///
+ internal interface IPriorityValueOwner
+ {
+ ///
+ /// Called when a 's value changes.
+ ///
+ /// The source of the change.
+ /// The old value.
+ /// The new value.
+ void Changed(PriorityValue sender, object oldValue, object newValue);
+ }
+}
diff --git a/src/Perspex.Base/Perspex.Base.csproj b/src/Perspex.Base/Perspex.Base.csproj
index ae185b3abf..df240ee3b8 100644
--- a/src/Perspex.Base/Perspex.Base.csproj
+++ b/src/Perspex.Base/Perspex.Base.csproj
@@ -43,6 +43,7 @@
Properties\SharedAssemblyInfo.cs
+
@@ -54,6 +55,7 @@
+
diff --git a/src/Perspex.Base/PerspexObject.cs b/src/Perspex.Base/PerspexObject.cs
index 32f8eedc91..0f28f598d8 100644
--- a/src/Perspex.Base/PerspexObject.cs
+++ b/src/Perspex.Base/PerspexObject.cs
@@ -21,7 +21,7 @@ namespace Perspex
///
/// This class is analogous to DependencyObject in WPF.
///
- public class PerspexObject : IPerspexObject, IPerspexObjectDebug, INotifyPropertyChanged
+ public class PerspexObject : IPerspexObject, IPerspexObjectDebug, INotifyPropertyChanged, IPriorityValueOwner
{
///
/// Maintains a list of direct property binding subscriptions so that the binding source
@@ -403,9 +403,9 @@ namespace Perspex
IDisposable subscription = null;
subscription = source
- .Select(x => TypeUtilities.CastOrDefault(x, property.PropertyType))
+ .Select(x => CastOrDefault(x, property.PropertyType))
.Do(_ => { }, () => s_directBindings.Remove(subscription))
- .Subscribe(x => SetValue(property, x));
+ .Subscribe(x => DirectBindingSet(property, x));
s_directBindings.Add(subscription);
@@ -477,6 +477,34 @@ namespace Perspex
}
}
+ ///
+ void IPriorityValueOwner.Changed(PriorityValue sender, object oldValue, object newValue)
+ {
+ var property = sender.Property;
+ var priority = (BindingPriority)sender.ValuePriority;
+
+ oldValue = (oldValue == PerspexProperty.UnsetValue) ?
+ GetDefaultValue(property) :
+ oldValue;
+ newValue = (newValue == PerspexProperty.UnsetValue) ?
+ GetDefaultValue(property) :
+ newValue;
+
+ if (!Equals(oldValue, newValue))
+ {
+ RaisePropertyChanged(property, oldValue, newValue, priority);
+
+ Logger.Verbose(
+ LogArea.Property,
+ this,
+ "{Property} changed from {$Old} to {$Value} with priority {Priority}",
+ property,
+ oldValue,
+ newValue,
+ priority);
+ }
+ }
+
///
Delegate[] IPerspexObjectDebug.GetPropertyChangedSubscribers()
{
@@ -587,6 +615,27 @@ namespace Perspex
}
}
+ ///
+ /// Tries to cast a value to a type, taking into account that the value may be a
+ /// .
+ ///
+ /// The value.
+ /// The type.
+ /// The cast value, or a .
+ private static object CastOrDefault(object value, Type type)
+ {
+ var error = value as BindingError;
+
+ if (error == null)
+ {
+ return TypeUtilities.CastOrDefault(value, type);
+ }
+ else
+ {
+ return error;
+ }
+ }
+
///
/// Creates a for a .
///
@@ -604,35 +653,42 @@ namespace Perspex
PriorityValue result = new PriorityValue(
this,
- property.Name,
+ property,
property.PropertyType,
validate2);
- result.Changed.Subscribe(x =>
+ return result;
+ }
+
+ ///
+ /// Sets a property value for a direct property binding.
+ ///
+ /// The property.
+ /// The value.
+ ///
+ private void DirectBindingSet(PerspexProperty property, object value)
+ {
+ var error = value as BindingError;
+
+ if (error == null)
+ {
+ SetValue(property, value);
+ }
+ else
{
- object oldValue = (x.Item1 == PerspexProperty.UnsetValue) ?
- GetDefaultValue(property) :
- x.Item1;
- object newValue = (x.Item2 == PerspexProperty.UnsetValue) ?
- GetDefaultValue(property) :
- x.Item2;
-
- if (!Equals(oldValue, newValue))
+ if (error.UseFallbackValue)
{
- RaisePropertyChanged(property, oldValue, newValue, (BindingPriority)result.ValuePriority);
-
- Logger.Verbose(
- LogArea.Property,
- this,
- "{Property} changed from {$Old} to {$Value} with priority {Priority}",
- property,
- oldValue,
- newValue,
- (BindingPriority)result.ValuePriority);
+ SetValue(property, error.FallbackValue);
}
- });
- return result;
+ Logger.Error(
+ LogArea.Binding,
+ this,
+ "Error binding to {Target}.{Property}: {Message}",
+ this,
+ property,
+ error.Exception.Message);
+ }
}
///
diff --git a/src/Perspex.Base/PerspexProperty.cs b/src/Perspex.Base/PerspexProperty.cs
index e88c7f4418..36c534266e 100644
--- a/src/Perspex.Base/PerspexProperty.cs
+++ b/src/Perspex.Base/PerspexProperty.cs
@@ -71,7 +71,11 @@ namespace Perspex
///
/// The direct property to copy.
/// The new owner type.
- protected PerspexProperty(PerspexProperty source, Type ownerType)
+ /// Optional overridden metadata.
+ protected PerspexProperty(
+ PerspexProperty source,
+ Type ownerType,
+ PropertyMetadata metadata)
{
Contract.Requires(source != null);
Contract.Requires(ownerType != null);
@@ -86,6 +90,11 @@ namespace Perspex
Notifying = source.Notifying;
Id = source.Id;
_defaultMetadata = source._defaultMetadata;
+
+ if (metadata != null)
+ {
+ _metadata.Add(ownerType, metadata);
+ }
}
///
diff --git a/src/Perspex.Base/PerspexProperty`1.cs b/src/Perspex.Base/PerspexProperty`1.cs
index 101aeaed22..1ad1cef2e1 100644
--- a/src/Perspex.Base/PerspexProperty`1.cs
+++ b/src/Perspex.Base/PerspexProperty`1.cs
@@ -32,8 +32,12 @@ namespace Perspex
///
/// The property to copy.
/// The new owner type.
- protected PerspexProperty(PerspexProperty source, Type ownerType)
- : base(source, ownerType)
+ /// Optional overridden metadata.
+ protected PerspexProperty(
+ PerspexProperty source,
+ Type ownerType,
+ PropertyMetadata metadata)
+ : base(source, ownerType, metadata)
{
}
}
diff --git a/src/Perspex.Base/PriorityBindingEntry.cs b/src/Perspex.Base/PriorityBindingEntry.cs
index a17080f0f4..be6c451a71 100644
--- a/src/Perspex.Base/PriorityBindingEntry.cs
+++ b/src/Perspex.Base/PriorityBindingEntry.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
+using Perspex.Data;
namespace Perspex
{
@@ -10,19 +11,19 @@ namespace Perspex
///
internal class PriorityBindingEntry : IDisposable
{
- ///
- /// The binding subscription.
- ///
+ private PriorityLevel _owner;
private IDisposable _subscription;
///
/// Initializes a new instance of the class.
///
+ /// The owner.
///
/// The binding index. Later bindings should have higher indexes.
///
- public PriorityBindingEntry(int index)
+ public PriorityBindingEntry(PriorityLevel owner, int index)
{
+ _owner = owner;
Index = index;
}
@@ -61,16 +62,9 @@ namespace Perspex
/// Starts listening to the binding.
///
/// The binding.
- /// Called when the binding changes.
- /// Called when the binding completes.
- public void Start(
- IObservable