Browse Source

Added BindingError class.

pull/494/merge
Steven Kirk 11 years ago
parent
commit
3bb9fe0b58
  1. 32
      src/Perspex.Base/Data/BindingError.cs
  2. 1
      src/Perspex.Base/Perspex.Base.csproj
  3. 52
      src/Perspex.Base/PerspexObject.cs
  4. 18
      src/Perspex.Base/PriorityBindingEntry.cs
  5. 24
      src/Perspex.Base/PriorityLevel.cs
  6. 22
      src/Perspex.Base/PriorityValue.cs
  7. 8
      tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj
  8. 33
      tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs
  9. 30
      tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs
  10. 1
      tests/Perspex.UnitTests/Perspex.UnitTests.csproj
  11. 38
      tests/Perspex.UnitTests/TestLogSink.cs

32
src/Perspex.Base/Data/BindingError.cs

@ -0,0 +1,32 @@
// 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
{
/// <summary>
/// Represents a recoverable binding error.
/// </summary>
/// <remarks>
/// When produced by a binding source observable, informs the binding system that an error
/// occurred. It causes a binding error to be logged: the value of the bound property will not
/// change.
/// </remarks>
public class BindingError
{
/// <summary>
/// Initializes a new instance of the <see cref="BindingError"/> class.
/// </summary>
/// <param name="exception">An exception describing the binding error.</param>
public BindingError(Exception exception)
{
Exception = exception;
}
/// <summary>
/// Gets the exception describing the binding error.
/// </summary>
public Exception Exception { get; }
}
}

1
src/Perspex.Base/Perspex.Base.csproj

@ -43,6 +43,7 @@
<Compile Include="..\Shared\SharedAssemblyInfo.cs"> <Compile Include="..\Shared\SharedAssemblyInfo.cs">
<Link>Properties\SharedAssemblyInfo.cs</Link> <Link>Properties\SharedAssemblyInfo.cs</Link>
</Compile> </Compile>
<Compile Include="Data\BindingError.cs" />
<Compile Include="Diagnostics\INotifyCollectionChangedDebug.cs" /> <Compile Include="Diagnostics\INotifyCollectionChangedDebug.cs" />
<Compile Include="Data\AssignBindingAttribute.cs" /> <Compile Include="Data\AssignBindingAttribute.cs" />
<Compile Include="Data\BindingOperations.cs" /> <Compile Include="Data\BindingOperations.cs" />

52
src/Perspex.Base/PerspexObject.cs

@ -403,9 +403,9 @@ namespace Perspex
IDisposable subscription = null; IDisposable subscription = null;
subscription = source subscription = source
.Select(x => TypeUtilities.CastOrDefault(x, property.PropertyType)) .Select(x => CastOrDefault(x, property.PropertyType))
.Do(_ => { }, () => s_directBindings.Remove(subscription)) .Do(_ => { }, () => s_directBindings.Remove(subscription))
.Subscribe(x => SetValue(property, x)); .Subscribe(x => DirectBindingSet(property, x));
s_directBindings.Add(subscription); s_directBindings.Add(subscription);
@ -587,6 +587,27 @@ namespace Perspex
} }
} }
/// <summary>
/// Tries to cast a value to a type, taking into account that the value may be a
/// <see cref="BindingError"/>.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="type">The type.</param>
/// <returns>The cast value, or a <see cref="BindingError"/>.</returns>
private static object CastOrDefault(object value, Type type)
{
var error = value as BindingError;
if (error == null)
{
return TypeUtilities.CastOrDefault(value, type);
}
else
{
return error;
}
}
/// <summary> /// <summary>
/// Creates a <see cref="PriorityValue"/> for a <see cref="PerspexProperty"/>. /// Creates a <see cref="PriorityValue"/> for a <see cref="PerspexProperty"/>.
/// </summary> /// </summary>
@ -635,6 +656,33 @@ namespace Perspex
return result; return result;
} }
/// <summary>
/// Sets a property value for a direct property binding.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="value">The value.</param>
/// <returns></returns>
private void DirectBindingSet(PerspexProperty property, object value)
{
var error = value as BindingError;
if (error == null)
{
SetValue(property, value);
}
else
{
Logger.Error(
LogArea.Binding,
this,
"Error binding to {Target}.{PropertyName}: {Message}",
property.Name,
property.PropertyType,
value,
value.GetType());
}
}
/// <summary> /// <summary>
/// Converts an unset value to the default value for a direct property. /// Converts an unset value to the default value for a direct property.
/// </summary> /// </summary>

18
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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using Perspex.Data;
namespace Perspex namespace Perspex
{ {
@ -63,10 +64,12 @@ namespace Perspex
/// <param name="binding">The binding.</param> /// <param name="binding">The binding.</param>
/// <param name="changed">Called when the binding changes.</param> /// <param name="changed">Called when the binding changes.</param>
/// <param name="completed">Called when the binding completes.</param> /// <param name="completed">Called when the binding completes.</param>
/// <param name="error">Called when a binding error occurs.</param>
public void Start( public void Start(
IObservable<object> binding, IObservable<object> binding,
Action<PriorityBindingEntry> changed, Action<PriorityBindingEntry> changed,
Action<PriorityBindingEntry> completed) Action<PriorityBindingEntry> completed,
Action<PriorityBindingEntry, BindingError> error)
{ {
Contract.Requires<ArgumentNullException>(binding != null); Contract.Requires<ArgumentNullException>(binding != null);
Contract.Requires<ArgumentNullException>(changed != null); Contract.Requires<ArgumentNullException>(changed != null);
@ -88,8 +91,17 @@ namespace Perspex
_subscription = binding.Subscribe( _subscription = binding.Subscribe(
value => value =>
{ {
Value = value; var bindingError = value as BindingError;
changed(this);
if (bindingError == null)
{
Value = value;
changed(this);
}
else if (error != null)
{
error(this, bindingError);
}
}, },
() => completed(this)); () => completed(this));
} }

24
src/Perspex.Base/PriorityLevel.cs

@ -4,6 +4,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using Perspex.Data;
using Perspex.Logging;
namespace Perspex namespace Perspex
{ {
@ -52,6 +54,11 @@ namespace Perspex
/// </summary> /// </summary>
private readonly Action<PriorityLevel> _changed; private readonly Action<PriorityLevel> _changed;
/// <summary>
/// Method called when a binding error occurs.
/// </summary>
private readonly Action<PriorityLevel, BindingError> _error;
/// <summary> /// <summary>
/// The current direct value. /// The current direct value.
/// </summary> /// </summary>
@ -70,15 +77,18 @@ namespace Perspex
/// <param name="priority">The priority.</param> /// <param name="priority">The priority.</param>
/// <param name="mode">The precedence mode.</param> /// <param name="mode">The precedence mode.</param>
/// <param name="changed">A method to be called when the current value changes.</param> /// <param name="changed">A method to be called when the current value changes.</param>
/// <param name="error">A method to be called when a binding error occurs.</param>
public PriorityLevel( public PriorityLevel(
int priority, int priority,
LevelPrecedenceMode mode, LevelPrecedenceMode mode,
Action<PriorityLevel> changed) Action<PriorityLevel> changed,
Action<PriorityLevel, BindingError> error)
{ {
Contract.Requires<ArgumentNullException>(changed != null); Contract.Requires<ArgumentNullException>(changed != null);
_mode = mode; _mode = mode;
_changed = changed; _changed = changed;
_error = error;
Priority = priority; Priority = priority;
Value = _directValue = PerspexProperty.UnsetValue; Value = _directValue = PerspexProperty.UnsetValue;
ActiveBindingIndex = -1; ActiveBindingIndex = -1;
@ -135,7 +145,7 @@ namespace Perspex
var entry = new PriorityBindingEntry(_nextIndex++); var entry = new PriorityBindingEntry(_nextIndex++);
var node = Bindings.AddFirst(entry); var node = Bindings.AddFirst(entry);
entry.Start(binding, Changed, Completed); entry.Start(binding, Changed, Completed, Error);
return Disposable.Create(() => return Disposable.Create(() =>
{ {
@ -184,6 +194,16 @@ namespace Perspex
} }
} }
/// <summary>
/// Invoked when an entry in <see cref="Bindings"/> encounters a recoverable error.
/// </summary>
/// <param name="entry">The entry that completed.</param>
/// <param name="error">The error.</param>
private void Error(PriorityBindingEntry entry, BindingError error)
{
_error(this, error);
}
/// <summary> /// <summary>
/// Activates the first binding that has a value. /// Activates the first binding that has a value.
/// </summary> /// </summary>

22
src/Perspex.Base/PriorityValue.cs

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using System.Text; using System.Text;
using Perspex.Data;
using Perspex.Logging; using Perspex.Logging;
using Perspex.Utilities; using Perspex.Utilities;
@ -210,7 +211,7 @@ namespace Perspex
if (!_levels.TryGetValue(priority, out result)) if (!_levels.TryGetValue(priority, out result))
{ {
var mode = (LevelPrecedenceMode)(priority % 2); var mode = (LevelPrecedenceMode)(priority % 2);
result = new PriorityLevel(priority, mode, ValueChanged); result = new PriorityLevel(priority, mode, ValueChanged, Error);
_levels.Add(priority, result); _levels.Add(priority, result);
} }
@ -242,7 +243,7 @@ namespace Perspex
else else
{ {
Logger.Error( Logger.Error(
LogArea.Property, LogArea.Binding,
_owner, _owner,
"Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})", "Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})",
_name, _name,
@ -279,5 +280,22 @@ namespace Perspex
} }
} }
} }
/// <summary>
/// Called when a priority level encounters an error.
/// </summary>
/// <param name="level">The priority level of the changed entry.</param>
/// <param name="error">The binding error.</param>
private void Error(PriorityLevel level, BindingError error)
{
Logger.Log(
LogEventLevel.Error,
LogArea.Binding,
_owner,
"Error binding to {Target}.{PropertyName}: {Message}",
_owner,
_name,
error.Exception.Message);
}
} }
} }

8
tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj

@ -61,10 +61,6 @@
<HintPath>..\..\packages\Rx-PlatformServices.2.2.5\lib\net45\System.Reactive.PlatformServices.dll</HintPath> <HintPath>..\..\packages\Rx-PlatformServices.2.2.5\lib\net45\System.Reactive.PlatformServices.dll</HintPath>
<Private>True</Private> <Private>True</Private>
</Reference> </Reference>
<Reference Include="System.Reactive.PlatformServices, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\packages\Rx-PlatformServices.2.2.5\lib\net45\System.Reactive.PlatformServices.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="xunit.abstractions, Version=2.0.0.0, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c"> <Reference Include="xunit.abstractions, Version=2.0.0.0, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c">
<HintPath>..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll</HintPath> <HintPath>..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll</HintPath>
</Reference> </Reference>
@ -124,6 +120,10 @@
<Project>{B09B78D8-9B26-48B0-9149-D64A2F120F3F}</Project> <Project>{B09B78D8-9B26-48B0-9149-D64A2F120F3F}</Project>
<Name>Perspex.Base</Name> <Name>Perspex.Base</Name>
</ProjectReference> </ProjectReference>
<ProjectReference Include="..\Perspex.UnitTests\Perspex.UnitTests.csproj">
<Project>{88060192-33d5-4932-b0f9-8bd2763e857d}</Project>
<Name>Perspex.UnitTests</Name>
</ProjectReference>
</ItemGroup> </ItemGroup>
<Choose> <Choose>
<When Condition="'$(VisualStudioVersion)' == '10.0' And '$(IsCodedUITest)' == 'True'"> <When Condition="'$(VisualStudioVersion)' == '10.0' And '$(IsCodedUITest)' == 'True'">

33
tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs

@ -2,10 +2,14 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System; using System;
using System.Collections;
using System.Collections.Generic;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using Microsoft.Reactive.Testing; using Microsoft.Reactive.Testing;
using Perspex.Data; using Perspex.Data;
using Perspex.Logging;
using Perspex.UnitTests;
using Xunit; using Xunit;
namespace Perspex.Base.UnitTests namespace Perspex.Base.UnitTests
@ -263,6 +267,35 @@ namespace Perspex.Base.UnitTests
Assert.Equal("first", target2.GetValue(Class1.FooProperty)); Assert.Equal("first", target2.GetValue(Class1.FooProperty));
} }
[Fact]
public void Bind_Logs_BindingError()
{
var target = new Class1();
var source = new Subject<object>();
var called = false;
var expectedMessageTemplate = "Error binding to {Target}.{PropertyName}: {Message}";
LogCallback checkLogMessage = (level, area, src, mt, pv) =>
{
if (level == LogEventLevel.Error &&
area == LogArea.Binding &&
mt == expectedMessageTemplate)
{
called = true;
}
};
using (TestLogSink.Start(checkLogMessage))
{
target.Bind(Class1.QuxProperty, source);
source.OnNext(6.7);
source.OnNext(new BindingError(new InvalidOperationException("Foo")));
Assert.Equal(6.7, target.GetValue(Class1.QuxProperty));
Assert.True(called);
}
}
/// <summary> /// <summary>
/// Returns an observable that returns a single value but does not complete. /// Returns an observable that returns a single value but does not complete.
/// </summary> /// </summary>

30
tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs

@ -5,6 +5,8 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using Perspex.Data; using Perspex.Data;
using Perspex.Logging;
using Perspex.UnitTests;
using Xunit; using Xunit;
namespace Perspex.Base.UnitTests namespace Perspex.Base.UnitTests
@ -394,6 +396,34 @@ namespace Perspex.Base.UnitTests
Assert.True(raised); Assert.True(raised);
} }
[Fact]
public void Binding_To_Direct_Property_Logs_BindingError()
{
var target = new Class1();
var source = new Subject<object>();
var called = false;
var expectedMessageTemplate = "Error binding to {Target}.{PropertyName}: {Message}";
LogCallback checkLogMessage = (level, area, src, mt, pv) =>
{
if (level == LogEventLevel.Error &&
area == LogArea.Binding &&
mt == expectedMessageTemplate)
{
called = true;
}
};
using (TestLogSink.Start(checkLogMessage))
{
target.Bind(Class1.FooProperty, source);
source.OnNext("baz");
source.OnNext(new BindingError(new InvalidOperationException("Foo")));
}
Assert.True(called);
}
private class Class1 : PerspexObject private class Class1 : PerspexObject
{ {
public static readonly DirectProperty<Class1, string> FooProperty = public static readonly DirectProperty<Class1, string> FooProperty =

1
tests/Perspex.UnitTests/Perspex.UnitTests.csproj

@ -63,6 +63,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="NotifyingBase.cs" /> <Compile Include="NotifyingBase.cs" />
<Compile Include="TestLogSink.cs" />
<Compile Include="TestTemplatedRoot.cs" /> <Compile Include="TestTemplatedRoot.cs" />
<Compile Include="TestRoot.cs" /> <Compile Include="TestRoot.cs" />
<Compile Include="TestServices.cs" /> <Compile Include="TestServices.cs" />

38
tests/Perspex.UnitTests/TestLogSink.cs

@ -0,0 +1,38 @@
// 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.Reactive.Disposables;
using Perspex.Logging;
namespace Perspex.UnitTests
{
public delegate void LogCallback(
LogEventLevel level,
string area,
object source,
string messageTemplate,
params object[] propertyValues);
public class TestLogSink : ILogSink
{
private LogCallback _callback;
public TestLogSink(LogCallback callback)
{
_callback = callback;
}
public static IDisposable Start(LogCallback callback)
{
var sink = new TestLogSink(callback);
Logger.Sink = sink;
return Disposable.Create(() => Logger.Sink = null);
}
public void Log(LogEventLevel level, string area, object source, string messageTemplate, params object[] propertyValues)
{
_callback(level, area, source, messageTemplate, propertyValues);
}
}
}
Loading…
Cancel
Save