Browse Source

Added BindingError class.

pull/494/merge
Steven Kirk 10 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">
<Link>Properties\SharedAssemblyInfo.cs</Link>
</Compile>
<Compile Include="Data\BindingError.cs" />
<Compile Include="Diagnostics\INotifyCollectionChangedDebug.cs" />
<Compile Include="Data\AssignBindingAttribute.cs" />
<Compile Include="Data\BindingOperations.cs" />

52
src/Perspex.Base/PerspexObject.cs

@ -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);
@ -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>
/// Creates a <see cref="PriorityValue"/> for a <see cref="PerspexProperty"/>.
/// </summary>
@ -635,6 +656,33 @@ namespace Perspex
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>
/// Converts an unset value to the default value for a direct property.
/// </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.
using System;
using Perspex.Data;
namespace Perspex
{
@ -63,10 +64,12 @@ namespace Perspex
/// <param name="binding">The binding.</param>
/// <param name="changed">Called when the binding changes.</param>
/// <param name="completed">Called when the binding completes.</param>
/// <param name="error">Called when a binding error occurs.</param>
public void Start(
IObservable<object> binding,
Action<PriorityBindingEntry> changed,
Action<PriorityBindingEntry> completed)
Action<PriorityBindingEntry> completed,
Action<PriorityBindingEntry, BindingError> error)
{
Contract.Requires<ArgumentNullException>(binding != null);
Contract.Requires<ArgumentNullException>(changed != null);
@ -88,8 +91,17 @@ namespace Perspex
_subscription = binding.Subscribe(
value =>
{
Value = value;
changed(this);
var bindingError = value as BindingError;
if (bindingError == null)
{
Value = value;
changed(this);
}
else if (error != null)
{
error(this, bindingError);
}
},
() => completed(this));
}

24
src/Perspex.Base/PriorityLevel.cs

@ -4,6 +4,8 @@
using System;
using System.Collections.Generic;
using System.Reactive.Disposables;
using Perspex.Data;
using Perspex.Logging;
namespace Perspex
{
@ -52,6 +54,11 @@ namespace Perspex
/// </summary>
private readonly Action<PriorityLevel> _changed;
/// <summary>
/// Method called when a binding error occurs.
/// </summary>
private readonly Action<PriorityLevel, BindingError> _error;
/// <summary>
/// The current direct value.
/// </summary>
@ -70,15 +77,18 @@ namespace Perspex
/// <param name="priority">The priority.</param>
/// <param name="mode">The precedence mode.</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(
int priority,
LevelPrecedenceMode mode,
Action<PriorityLevel> changed)
Action<PriorityLevel> changed,
Action<PriorityLevel, BindingError> error)
{
Contract.Requires<ArgumentNullException>(changed != null);
_mode = mode;
_changed = changed;
_error = error;
Priority = priority;
Value = _directValue = PerspexProperty.UnsetValue;
ActiveBindingIndex = -1;
@ -135,7 +145,7 @@ namespace Perspex
var entry = new PriorityBindingEntry(_nextIndex++);
var node = Bindings.AddFirst(entry);
entry.Start(binding, Changed, Completed);
entry.Start(binding, Changed, Completed, Error);
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>
/// Activates the first binding that has a value.
/// </summary>

22
src/Perspex.Base/PriorityValue.cs

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Reactive.Subjects;
using System.Text;
using Perspex.Data;
using Perspex.Logging;
using Perspex.Utilities;
@ -210,7 +211,7 @@ namespace Perspex
if (!_levels.TryGetValue(priority, out result))
{
var mode = (LevelPrecedenceMode)(priority % 2);
result = new PriorityLevel(priority, mode, ValueChanged);
result = new PriorityLevel(priority, mode, ValueChanged, Error);
_levels.Add(priority, result);
}
@ -242,7 +243,7 @@ namespace Perspex
else
{
Logger.Error(
LogArea.Property,
LogArea.Binding,
_owner,
"Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})",
_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>
<Private>True</Private>
</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">
<HintPath>..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll</HintPath>
</Reference>
@ -124,6 +120,10 @@
<Project>{B09B78D8-9B26-48B0-9149-D64A2F120F3F}</Project>
<Name>Perspex.Base</Name>
</ProjectReference>
<ProjectReference Include="..\Perspex.UnitTests\Perspex.UnitTests.csproj">
<Project>{88060192-33d5-4932-b0f9-8bd2763e857d}</Project>
<Name>Perspex.UnitTests</Name>
</ProjectReference>
</ItemGroup>
<Choose>
<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.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Microsoft.Reactive.Testing;
using Perspex.Data;
using Perspex.Logging;
using Perspex.UnitTests;
using Xunit;
namespace Perspex.Base.UnitTests
@ -263,6 +267,35 @@ namespace Perspex.Base.UnitTests
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>
/// Returns an observable that returns a single value but does not complete.
/// </summary>

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

@ -5,6 +5,8 @@ using System;
using System.Collections.Generic;
using System.Reactive.Subjects;
using Perspex.Data;
using Perspex.Logging;
using Perspex.UnitTests;
using Xunit;
namespace Perspex.Base.UnitTests
@ -394,6 +396,34 @@ namespace Perspex.Base.UnitTests
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
{
public static readonly DirectProperty<Class1, string> FooProperty =

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

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