Browse Source

Initial implementation of ExpressionObserver.

pull/237/head
Steven Kirk 11 years ago
parent
commit
005cd231cc
  1. 14
      Perspex.sln
  2. BIN
      Perspex.v2.ncrunchsolution
  3. 103
      src/Markup/Perspex.Markup/Binding/ExpressionNode.cs
  4. 74
      src/Markup/Perspex.Markup/Binding/ExpressionNodeBuilder.cs
  5. 55
      src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs
  6. 26
      src/Markup/Perspex.Markup/Binding/ExpressionValue.cs
  7. 67
      src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs
  8. 93
      src/Markup/Perspex.Markup/Perspex.Markup.csproj
  9. 30
      src/Markup/Perspex.Markup/Properties/AssemblyInfo.cs
  10. 13
      src/Markup/Perspex.Markup/packages.config
  11. 29
      tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests.cs
  12. 163
      tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests.cs
  13. 38
      tests/Perspex.Markup.UnitTests/Binding/NotifyingBase.cs
  14. 103
      tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj
  15. 36
      tests/Perspex.Markup.UnitTests/Properties/AssemblyInfo.cs
  16. 14
      tests/Perspex.Markup.UnitTests/packages.config

14
Perspex.sln

@ -96,6 +96,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.HtmlRenderer", "src
EndProject
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "PlatformSupport", "src\Shared\PlatformSupport\PlatformSupport.shproj", "{E4D9629C-F168-4224-3F51-A5E482FFBC42}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Markup", "src\Markup\Perspex.Markup\Perspex.Markup.csproj", "{6417E941-21BC-467B-A771-0DE389353CE6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Markup.UnitTests", "tests\Perspex.Markup.UnitTests\Perspex.Markup.UnitTests.csproj", "{8EF392D5-1416-45AA-9956-7CBBC3229E8A}"
EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
src\Shared\PlatformSupport\PlatformSupport.projitems*{e4d9629c-f168-4224-3f51-a5e482ffbc42}*SharedItemsImports = 13
@ -235,6 +239,14 @@ Global
{5FB2B005-0A7F-4DAD-ADD4-3ED01444E63D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5FB2B005-0A7F-4DAD-ADD4-3ED01444E63D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5FB2B005-0A7F-4DAD-ADD4-3ED01444E63D}.Release|Any CPU.Build.0 = Release|Any CPU
{6417E941-21BC-467B-A771-0DE389353CE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6417E941-21BC-467B-A771-0DE389353CE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6417E941-21BC-467B-A771-0DE389353CE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6417E941-21BC-467B-A771-0DE389353CE6}.Release|Any CPU.Build.0 = Release|Any CPU
{8EF392D5-1416-45AA-9956-7CBBC3229E8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8EF392D5-1416-45AA-9956-7CBBC3229E8A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8EF392D5-1416-45AA-9956-7CBBC3229E8A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8EF392D5-1416-45AA-9956-7CBBC3229E8A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -260,5 +272,7 @@ Global
{54F237D5-A70A-4752-9656-0C70B1A7B047} = {B9894058-278A-46B5-B6ED-AD613FCC03B3}
{FB05AC90-89BA-4F2F-A924-F37875FB547C} = {B9894058-278A-46B5-B6ED-AD613FCC03B3}
{E4D9629C-F168-4224-3F51-A5E482FFBC42} = {A689DEF5-D50F-4975-8B72-124C9EB54066}
{6417E941-21BC-467B-A771-0DE389353CE6} = {8B6A8209-894F-4BA1-B880-965FD453982C}
{8EF392D5-1416-45AA-9956-7CBBC3229E8A} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
EndGlobalSection
EndGlobal

BIN
Perspex.v2.ncrunchsolution

Binary file not shown.

103
src/Markup/Perspex.Markup/Binding/ExpressionNode.cs

@ -0,0 +1,103 @@
// 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;
using System.Reactive.Subjects;
namespace Perspex.Markup.Binding
{
public abstract class ExpressionNode : IObservable<ExpressionValue>
{
private object _target;
private Subject<ExpressionValue> _subject;
private ExpressionValue _value = ExpressionValue.None;
public ExpressionNode Next
{
get;
set;
}
public object Target
{
get { return _target; }
set
{
if (_target != null)
{
Unsubscribe(_target);
}
_target = value;
if (_target != null)
{
Subscribe(_target);
}
else
{
CurrentValue = ExpressionValue.None;
}
if (Next != null)
{
Next.Target = CurrentValue.Value;
}
}
}
public ExpressionValue CurrentValue
{
get
{
return _value;
}
set
{
if (value == null)
{
throw new ArgumentNullException("value");
}
_value = value;
if (Next != null)
{
Next.Target = value.Value;
}
if (_subject != null)
{
_subject.OnNext(value);
}
}
}
public IDisposable Subscribe(IObserver<ExpressionValue> observer)
{
if (Next != null)
{
return Next.Subscribe(observer);
}
else
{
if (_subject == null)
{
_subject = new Subject<ExpressionValue>();
}
observer.OnNext(CurrentValue);
return _subject.Subscribe(observer);
}
}
protected abstract void Subscribe(object target);
protected abstract void Unsubscribe(object target);
}
}

74
src/Markup/Perspex.Markup/Binding/ExpressionNodeBuilder.cs

@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Perspex.Markup.Binding
{
public class ExpressionNodeBuilder
{
public static IList<ExpressionNode> Build(string expression)
{
if (string.IsNullOrWhiteSpace(expression))
{
throw new ArgumentException("'expression' may not be empty.");
}
var syntaxTree = CSharpSyntaxTree.ParseText(expression, new CSharpParseOptions(kind: SourceCodeKind.Interactive));
var syntaxRoot = syntaxTree.GetRoot();
var syntax = syntaxRoot.ChildNodes().SingleOrDefault()?.ChildNodes()?.SingleOrDefault() as ExpressionStatementSyntax;
if (syntax != null)
{
var result = new List<ExpressionNode>();
foreach (SyntaxNode node in syntax.ChildNodes())
{
var identifier = node as IdentifierNameSyntax;
var memberAccess = node as MemberAccessExpressionSyntax;
if (identifier != null)
{
result.Add(new PropertyAccessorNode(identifier.Identifier.ValueText));
}
else if (memberAccess != null)
{
Build(memberAccess, result);
}
}
for (int i = 0; i < result.Count - 1; ++i)
{
result[i].Next = result[i + 1];
}
return result;
}
else
{
throw new Exception($"Invalid expression: {expression}");
}
}
private static void Build(MemberAccessExpressionSyntax syntax, IList<ExpressionNode> result)
{
foreach (SyntaxNode node in syntax.ChildNodes())
{
var identifier = node as IdentifierNameSyntax;
var memberAccess = node as MemberAccessExpressionSyntax;
if (identifier != null)
{
result.Add(new PropertyAccessorNode(identifier.Identifier.ValueText));
}
else if (memberAccess != null)
{
Build(memberAccess, result);
}
}
}
}
}

55
src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs

@ -0,0 +1,55 @@
// 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.Linq;
using System.Reactive;
using System.Reactive.Disposables;
namespace Perspex.Markup.Binding
{
public class ExpressionObserver : ObservableBase<ExpressionValue>
{
private int _count;
public ExpressionObserver(object root, string expression)
{
Root = root;
Nodes = ExpressionNodeBuilder.Build(expression);
}
public object Root { get; }
public IList<ExpressionNode> Nodes { get; }
protected override IDisposable SubscribeCore(IObserver<ExpressionValue> observer)
{
IncrementCount();
var subscription = Nodes[0].Subscribe(observer);
return Disposable.Create(() =>
{
DecrementCount();
subscription.Dispose();
});
}
private void IncrementCount()
{
if (_count++ == 0)
{
Nodes[0].Target = Root;
}
}
private void DecrementCount()
{
if (--_count == 0)
{
Nodes[0].Target = null;
}
}
}
}

26
src/Markup/Perspex.Markup/Binding/ExpressionValue.cs

@ -0,0 +1,26 @@
// 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.Binding
{
public class ExpressionValue
{
public static readonly ExpressionValue None = new ExpressionValue();
public ExpressionValue(object value)
{
HasValue = true;
Value = value;
}
private ExpressionValue()
{
HasValue = false;
}
public bool HasValue { get; }
public object Value { get; }
}
}

67
src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs

@ -0,0 +1,67 @@
// 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.Reflection;
namespace Perspex.Markup.Binding
{
public class PropertyAccessorNode : ExpressionNode
{
private PropertyInfo _propertyInfo;
public PropertyAccessorNode(string propertyName)
{
PropertyName = propertyName;
}
public string PropertyName { get; }
protected override void Subscribe(object target)
{
var result = ExpressionValue.None;
if (target != null)
{
_propertyInfo = target.GetType().GetTypeInfo().GetDeclaredProperty(PropertyName);
if (_propertyInfo != null)
{
result = new ExpressionValue(_propertyInfo.GetValue(target));
var inpc = target as INotifyPropertyChanged;
if (inpc != null)
{
inpc.PropertyChanged += PropertyChanged;
}
}
}
else
{
_propertyInfo = null;
}
CurrentValue = result;
}
private void PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == PropertyName)
{
CurrentValue = new ExpressionValue(_propertyInfo.GetValue(Target));
}
}
protected override void Unsubscribe(object target)
{
var inpc = target as INotifyPropertyChanged;
if (inpc != null)
{
inpc.PropertyChanged -= PropertyChanged;
}
}
}
}

93
src/Markup/Perspex.Markup/Perspex.Markup.csproj

@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<MinimumVisualStudioVersion>11.0</MinimumVisualStudioVersion>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{6417E941-21BC-467B-A771-0DE389353CE6}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Perspex.Markup</RootNamespace>
<AssemblyName>Perspex.Markup</AssemblyName>
<DefaultLanguage>en-US</DefaultLanguage>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<TargetFrameworkProfile>Profile7</TargetFrameworkProfile>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Compile Include="Binding\ExpressionNodeBuilder.cs" />
<Compile Include="Binding\ExpressionValue.cs" />
<Compile Include="Binding\PropertyAccessorNode.cs" />
<Compile Include="Binding\ExpressionNode.cs" />
<Compile Include="Binding\ExpressionObserver.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.CodeAnalysis, Version=1.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\..\packages\Microsoft.CodeAnalysis.Common.1.1.0-beta1-20150812-01\lib\portable-net45+win8\Microsoft.CodeAnalysis.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Microsoft.CodeAnalysis.CSharp, Version=1.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\..\packages\Microsoft.CodeAnalysis.CSharp.1.1.0-beta1-20150812-01\lib\portable-net45+win8\Microsoft.CodeAnalysis.CSharp.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Collections.Immutable, Version=1.1.36.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\..\..\packages\System.Collections.Immutable.1.1.36\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Reactive.Core, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\..\packages\Rx-Core.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Core.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Reactive.Interfaces, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\..\packages\Rx-Interfaces.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Interfaces.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Reactive.Linq, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\..\packages\Rx-Linq.2.2.5\lib\portable-windows8+net45+wp8\System.Reactive.Linq.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\portable-windows8+net45+wp8\System.Reactive.PlatformServices.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Reflection.Metadata, Version=1.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<HintPath>..\..\..\packages\System.Reflection.Metadata.1.1.0-alpha-00009\lib\portable-net45+win8\System.Reflection.Metadata.dll</HintPath>
<Private>True</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<Analyzer Include="..\..\..\packages\Microsoft.CodeAnalysis.Analyzers.1.1.0-beta1-20150812-01\analyzers\dotnet\cs\Microsoft.CodeAnalysis.Analyzers.dll" />
<Analyzer Include="..\..\..\packages\Microsoft.CodeAnalysis.Analyzers.1.1.0-beta1-20150812-01\analyzers\dotnet\cs\Microsoft.CodeAnalysis.CSharp.Analyzers.dll" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

30
src/Markup/Perspex.Markup/Properties/AssemblyInfo.cs

@ -0,0 +1,30 @@
using System.Resources;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Perspex.Markup")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Perspex.Markup")]
[assembly: AssemblyCopyright("Copyright © 2015")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: NeutralResourcesLanguage("en")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

13
src/Markup/Perspex.Markup/packages.config

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.CodeAnalysis.Analyzers" version="1.1.0-beta1-20150812-01" targetFramework="portable45-net45+win8" />
<package id="Microsoft.CodeAnalysis.Common" version="1.1.0-beta1-20150812-01" targetFramework="portable45-net45+win8" />
<package id="Microsoft.CodeAnalysis.CSharp" version="1.1.0-beta1-20150812-01" targetFramework="portable45-net45+win8" />
<package id="Rx-Core" version="2.2.5" targetFramework="portable45-net45+win8" />
<package id="Rx-Interfaces" version="2.2.5" targetFramework="portable45-net45+win8" />
<package id="Rx-Linq" version="2.2.5" targetFramework="portable45-net45+win8" />
<package id="Rx-Main" version="2.2.5" targetFramework="portable45-net45+win8" />
<package id="Rx-PlatformServices" version="2.2.5" targetFramework="portable45-net45+win8" />
<package id="System.Collections.Immutable" version="1.1.36" targetFramework="portable45-net45+win8" />
<package id="System.Reflection.Metadata" version="1.1.0-alpha-00009" targetFramework="portable45-net45+win8" />
</packages>

29
tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests.cs

@ -0,0 +1,29 @@
// 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 Perspex.Markup.Binding;
using Xunit;
namespace Perspex.Markup.UnitTests.Binding
{
public class ExpressionNodeBuilderTests
{
[Fact]
public void Should_Build_Single_Property()
{
var result = ExpressionNodeBuilder.Build("Foo");
Assert.Equal(1, result.Count);
Assert.IsType<PropertyAccessorNode>(result[0]);
}
[Fact]
public void Should_Build_Property_Chain()
{
var result = ExpressionNodeBuilder.Build("Foo.Bar.Baz");
Assert.Equal(3, result.Count);
Assert.IsType<PropertyAccessorNode>(result[0]);
}
}
}

163
tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests.cs

@ -0,0 +1,163 @@
// 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.Linq;
using Perspex.Markup.Binding;
using Xunit;
namespace Perspex.Markup.UnitTests.Binding
{
public class ExpressionObserverTests
{
[Fact]
public async void Should_Get_Simple_Property_Value()
{
var data = new { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo");
var result = await target.Take(1);
Assert.True(result.HasValue);
Assert.Equal("foo", result.Value);
}
[Fact]
public async void Should_Get_Simple_Property_Chain()
{
var data = new { Foo = new { Bar = new { Baz = "baz" } } };
var target = new ExpressionObserver(data, "Foo.Bar.Baz");
var result = await target.Take(1);
Assert.True(result.HasValue);
Assert.Equal("baz", result.Value);
}
[Fact]
public async void Should_Not_Have_Value_For_Broken_Chain()
{
var data = new { Foo = new { Bar = 1 } };
var target = new ExpressionObserver(data, "Foo.Bar.Baz");
var result = await target.Take(1);
Assert.False(result.HasValue);
}
[Fact]
public void Should_Track_Simple_Property_Value()
{
var data = new Class1 { Foo = "foo" };
var target = new ExpressionObserver(data, "Foo");
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x.Value));
data.Foo = "bar";
Assert.Equal(new[] { "foo", "bar" }, result);
sub.Dispose();
Assert.Equal(0, data.SubscriptionCount);
}
[Fact]
public void Should_Track_End_Of_Property_Chain_Changing()
{
var data = new Class1 { Class2 = new Class2 { Bar = "bar" } };
var target = new ExpressionObserver(data, "Class2.Bar");
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x.Value));
data.Class2.Bar = "baz";
Assert.Equal(new[] { "bar", "baz" }, result);
sub.Dispose();
Assert.Equal(0, data.SubscriptionCount);
Assert.Equal(0, data.Class2.SubscriptionCount);
}
[Fact]
public void Should_Track_Middle_Of_Property_Chain_Changing()
{
var data = new Class1 { Class2 = new Class2 { Bar = "bar" } };
var target = new ExpressionObserver(data, "Class2.Bar");
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x.Value));
var old = data.Class2;
data.Class2 = new Class2 { Bar = "baz" };
Assert.Equal(new[] { "bar", "baz" }, result);
sub.Dispose();
Assert.Equal(0, data.SubscriptionCount);
Assert.Equal(0, data.Class2.SubscriptionCount);
Assert.Equal(0, old.SubscriptionCount);
}
[Fact]
public void Should_Track_Middle_Of_Property_Chain_Breaking_Then_Mending()
{
var data = new Class1 { Class2 = new Class2 { Bar = "bar" } };
var target = new ExpressionObserver(data, "Class2.Bar");
var result = new List<object>();
var sub = target.Subscribe(x => result.Add(x.Value));
var old = data.Class2;
data.Class2 = null;
data.Class2 = new Class2 { Bar = "baz" };
Assert.Equal(new[] { "bar", null, "baz" }, result);
sub.Dispose();
Assert.Equal(0, data.SubscriptionCount);
Assert.Equal(0, data.Class2.SubscriptionCount);
Assert.Equal(0, old.SubscriptionCount);
}
private class Class1 : NotifyingBase
{
private string _foo;
private Class2 _class2;
public string Foo
{
get { return _foo; }
set
{
_foo = value;
RaisePropertyChanged(nameof(Foo));
}
}
public Class2 Class2
{
get { return _class2; }
set
{
_class2 = value;
RaisePropertyChanged(nameof(Class2));
}
}
}
private class Class2 : NotifyingBase
{
private string _bar;
public string Bar
{
get { return _bar; }
set
{
_bar = value;
RaisePropertyChanged(nameof(Bar));
}
}
}
}
}

38
tests/Perspex.Markup.UnitTests/Binding/NotifyingBase.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.ComponentModel;
namespace Perspex.Markup.UnitTests.Binding
{
public class NotifyingBase : INotifyPropertyChanged
{
private PropertyChangedEventHandler _propertyChanged;
public event PropertyChangedEventHandler PropertyChanged
{
add
{
_propertyChanged += value;
++SubscriptionCount;
}
remove
{
_propertyChanged -= value;
--SubscriptionCount;
}
}
public int SubscriptionCount
{
get;
private set;
}
protected void RaisePropertyChanged(string propertyName)
{
_propertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

103
tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj

@ -0,0 +1,103 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\packages\xunit.runner.visualstudio.2.0.1\build\net20\xunit.runner.visualstudio.props" Condition="Exists('..\..\packages\xunit.runner.visualstudio.2.0.1\build\net20\xunit.runner.visualstudio.props')" />
<Import Project="..\..\packages\xunit.core.2.0.0\build\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.props" Condition="Exists('..\..\packages\xunit.core.2.0.0\build\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.props')" />
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{8EF392D5-1416-45AA-9956-7CBBC3229E8A}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Perspex.Markup.UnitTests</RootNamespace>
<AssemblyName>Perspex.Markup.UnitTests</AssemblyName>
<TargetFrameworkVersion>v4.6</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Reactive.Core, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\packages\Rx-Core.2.2.5\lib\net45\System.Reactive.Core.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Reactive.Interfaces, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\packages\Rx-Interfaces.2.2.5\lib\net45\System.Reactive.Interfaces.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System.Reactive.Linq, Version=2.2.5.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<HintPath>..\..\packages\Rx-Linq.2.2.5\lib\net45\System.Reactive.Linq.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="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
<Reference Include="xunit.abstractions, Version=2.0.0.0, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL">
<HintPath>..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="xunit.assert, Version=2.0.0.2929, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL">
<HintPath>..\..\packages\xunit.assert.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="xunit.core, Version=2.0.0.2929, Culture=neutral, PublicKeyToken=8d05b1bb7a6fdb6c, processorArchitecture=MSIL">
<HintPath>..\..\packages\xunit.extensibility.core.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.dll</HintPath>
<Private>True</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Binding\ExpressionObserverTests.cs" />
<Compile Include="Binding\ExpressionNodeBuilderTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Markup\Perspex.Markup\Perspex.Markup.csproj">
<Project>{6417e941-21bc-467b-a771-0de389353ce6}</Project>
<Name>Perspex.Markup</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\packages\xunit.core.2.0.0\build\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\xunit.core.2.0.0\build\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.props'))" />
<Error Condition="!Exists('..\..\packages\xunit.runner.visualstudio.2.0.1\build\net20\xunit.runner.visualstudio.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\packages\xunit.runner.visualstudio.2.0.1\build\net20\xunit.runner.visualstudio.props'))" />
</Target>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

36
tests/Perspex.Markup.UnitTests/Properties/AssemblyInfo.cs

@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Perspex.Markup.UnitTests")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Perspex.Markup.UnitTests")]
[assembly: AssemblyCopyright("Copyright © 2015")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("8ef392d5-1416-45aa-9956-7cbbc3229e8a")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

14
tests/Perspex.Markup.UnitTests/packages.config

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Rx-Core" version="2.2.5" targetFramework="net46" />
<package id="Rx-Interfaces" version="2.2.5" targetFramework="net46" />
<package id="Rx-Linq" version="2.2.5" targetFramework="net46" />
<package id="Rx-Main" version="2.2.5" targetFramework="net46" />
<package id="Rx-PlatformServices" version="2.2.5" targetFramework="net46" />
<package id="xunit" version="2.0.0" targetFramework="net46" />
<package id="xunit.abstractions" version="2.0.0" targetFramework="net46" />
<package id="xunit.assert" version="2.0.0" targetFramework="net46" />
<package id="xunit.core" version="2.0.0" targetFramework="net46" />
<package id="xunit.extensibility.core" version="2.0.0" targetFramework="net46" />
<package id="xunit.runner.visualstudio" version="2.0.1" targetFramework="net46" />
</packages>
Loading…
Cancel
Save