16 changed files with 858 additions and 0 deletions
Binary file not shown.
@ -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); |
|||
|
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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")] |
|||
@ -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> |
|||
@ -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]); |
|||
} |
|||
} |
|||
} |
|||
@ -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)); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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)); |
|||
} |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -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")] |
|||
@ -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…
Reference in new issue