109 changed files with 3358 additions and 1059 deletions
@ -0,0 +1,6 @@ |
|||
<?xml version="1.0" encoding="utf-8" ?> |
|||
<configuration> |
|||
<startup> |
|||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6" /> |
|||
</startup> |
|||
</configuration> |
|||
@ -0,0 +1,40 @@ |
|||
using System; |
|||
using Perspex; |
|||
using Perspex.Controls; |
|||
using Perspex.Diagnostics; |
|||
using Perspex.Themes.Default; |
|||
using Serilog; |
|||
using Serilog.Filters; |
|||
|
|||
namespace BindingTest |
|||
{ |
|||
public class App : Application |
|||
{ |
|||
public App() |
|||
{ |
|||
RegisterServices(); |
|||
InitializeSubsystems((int)Environment.OSVersion.Platform); |
|||
Styles = new DefaultTheme(); |
|||
|
|||
Log.Logger = new LoggerConfiguration() |
|||
.Filter.ByIncludingOnly(Matching.WithProperty("Area", "Property")) |
|||
.Filter.ByIncludingOnly(Matching.WithProperty("Property", "Text")) |
|||
.MinimumLevel.Verbose() |
|||
.WriteTo.Trace(outputTemplate: "[{Id:X8}] [{SourceContext}] {Message}") |
|||
.CreateLogger(); |
|||
} |
|||
|
|||
public static void AttachDevTools(Window window) |
|||
{ |
|||
DevTools.Attach(window); |
|||
} |
|||
|
|||
private static void Main() |
|||
{ |
|||
var app = new App(); |
|||
var window = new MainWindow(); |
|||
window.Show(); |
|||
app.Run(window); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,167 @@ |
|||
<?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> |
|||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> |
|||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> |
|||
<ProjectGuid>{08B3E6B9-1CD5-443C-9F61-6D49D1C5F162}</ProjectGuid> |
|||
<OutputType>WinExe</OutputType> |
|||
<AppDesignerFolder>Properties</AppDesignerFolder> |
|||
<RootNamespace>BindingTest</RootNamespace> |
|||
<AssemblyName>BindingTest</AssemblyName> |
|||
<TargetFrameworkVersion>v4.6</TargetFrameworkVersion> |
|||
<FileAlignment>512</FileAlignment> |
|||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects> |
|||
</PropertyGroup> |
|||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> |
|||
<PlatformTarget>AnyCPU</PlatformTarget> |
|||
<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' "> |
|||
<PlatformTarget>AnyCPU</PlatformTarget> |
|||
<DebugType>pdbonly</DebugType> |
|||
<Optimize>true</Optimize> |
|||
<OutputPath>bin\Release\</OutputPath> |
|||
<DefineConstants>TRACE</DefineConstants> |
|||
<ErrorReport>prompt</ErrorReport> |
|||
<WarningLevel>4</WarningLevel> |
|||
</PropertyGroup> |
|||
<PropertyGroup> |
|||
<StartupObject /> |
|||
</PropertyGroup> |
|||
<ItemGroup> |
|||
<Reference Include="Serilog, Version=1.5.0.0, Culture=neutral, PublicKeyToken=24c2f752a8e58a10, processorArchitecture=MSIL"> |
|||
<HintPath>..\..\packages\Serilog.1.5.9\lib\net45\Serilog.dll</HintPath> |
|||
<Private>True</Private> |
|||
</Reference> |
|||
<Reference Include="Serilog.FullNetFx, Version=1.5.0.0, Culture=neutral, PublicKeyToken=24c2f752a8e58a10, processorArchitecture=MSIL"> |
|||
<HintPath>..\..\packages\Serilog.1.5.9\lib\net45\Serilog.FullNetFx.dll</HintPath> |
|||
<Private>True</Private> |
|||
</Reference> |
|||
<Reference Include="Splat, Version=1.6.2.0, Culture=neutral, processorArchitecture=MSIL"> |
|||
<HintPath>..\..\packages\Splat.1.6.2\lib\Net45\Splat.dll</HintPath> |
|||
<Private>True</Private> |
|||
</Reference> |
|||
<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" /> |
|||
</ItemGroup> |
|||
<ItemGroup> |
|||
<Compile Include="App.cs" /> |
|||
<Compile Include="MainWindow.paml.cs"> |
|||
<DependentUpon>MainWindow.paml</DependentUpon> |
|||
</Compile> |
|||
<Compile Include="Properties\AssemblyInfo.cs" /> |
|||
<Compile Include="ViewModels\MainWindowViewModel.cs" /> |
|||
<Compile Include="ViewModels\TestItem.cs" /> |
|||
</ItemGroup> |
|||
<ItemGroup> |
|||
<None Include="App.config" /> |
|||
<EmbeddedResource Include="MainWindow.paml"> |
|||
<SubType>Designer</SubType> |
|||
<Generator>MSBuild:Compile</Generator> |
|||
</EmbeddedResource> |
|||
<None Include="packages.config" /> |
|||
</ItemGroup> |
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\src\Markup\Perspex.Markup.Xaml\Perspex.Markup.Xaml.csproj"> |
|||
<Project>{3e53a01a-b331-47f3-b828-4a5717e77a24}</Project> |
|||
<Name>Perspex.Markup.Xaml</Name> |
|||
</ProjectReference> |
|||
<ProjectReference Include="..\..\src\Markup\Perspex.Markup\Perspex.Markup.csproj"> |
|||
<Project>{6417e941-21bc-467b-a771-0de389353ce6}</Project> |
|||
<Name>Perspex.Markup</Name> |
|||
</ProjectReference> |
|||
<ProjectReference Include="..\..\src\Perspex.Animation\Perspex.Animation.csproj"> |
|||
<Project>{d211e587-d8bc-45b9-95a4-f297c8fa5200}</Project> |
|||
<Name>Perspex.Animation</Name> |
|||
</ProjectReference> |
|||
<ProjectReference Include="..\..\src\Perspex.Application\Perspex.Application.csproj"> |
|||
<Project>{799a7bb5-3c2c-48b6-85a7-406a12c420da}</Project> |
|||
<Name>Perspex.Application</Name> |
|||
</ProjectReference> |
|||
<ProjectReference Include="..\..\src\Perspex.Base\Perspex.Base.csproj"> |
|||
<Project>{b09b78d8-9b26-48b0-9149-d64a2f120f3f}</Project> |
|||
<Name>Perspex.Base</Name> |
|||
</ProjectReference> |
|||
<ProjectReference Include="..\..\src\Perspex.Controls\Perspex.Controls.csproj"> |
|||
<Project>{d2221c82-4a25-4583-9b43-d791e3f6820c}</Project> |
|||
<Name>Perspex.Controls</Name> |
|||
</ProjectReference> |
|||
<ProjectReference Include="..\..\src\Perspex.Diagnostics\Perspex.Diagnostics.csproj"> |
|||
<Project>{7062ae20-5dcc-4442-9645-8195bdece63e}</Project> |
|||
<Name>Perspex.Diagnostics</Name> |
|||
</ProjectReference> |
|||
<ProjectReference Include="..\..\src\Perspex.Input\Perspex.Input.csproj"> |
|||
<Project>{62024b2d-53eb-4638-b26b-85eeaa54866e}</Project> |
|||
<Name>Perspex.Input</Name> |
|||
</ProjectReference> |
|||
<ProjectReference Include="..\..\src\Perspex.Interactivity\Perspex.Interactivity.csproj"> |
|||
<Project>{6b0ed19d-a08b-461c-a9d9-a9ee40b0c06b}</Project> |
|||
<Name>Perspex.Interactivity</Name> |
|||
</ProjectReference> |
|||
<ProjectReference Include="..\..\src\Perspex.Layout\Perspex.Layout.csproj"> |
|||
<Project>{42472427-4774-4c81-8aff-9f27b8e31721}</Project> |
|||
<Name>Perspex.Layout</Name> |
|||
</ProjectReference> |
|||
<ProjectReference Include="..\..\src\Perspex.ReactiveUI\Perspex.ReactiveUI.csproj"> |
|||
<Project>{6417b24e-49c2-4985-8db2-3ab9d898ec91}</Project> |
|||
<Name>Perspex.ReactiveUI</Name> |
|||
</ProjectReference> |
|||
<ProjectReference Include="..\..\src\Perspex.SceneGraph\Perspex.SceneGraph.csproj"> |
|||
<Project>{eb582467-6abb-43a1-b052-e981ba910e3a}</Project> |
|||
<Name>Perspex.SceneGraph</Name> |
|||
</ProjectReference> |
|||
<ProjectReference Include="..\..\src\Perspex.Styling\Perspex.Styling.csproj"> |
|||
<Project>{f1baa01a-f176-4c6a-b39d-5b40bb1b148f}</Project> |
|||
<Name>Perspex.Styling</Name> |
|||
</ProjectReference> |
|||
<ProjectReference Include="..\..\src\Perspex.Themes.Default\Perspex.Themes.Default.csproj"> |
|||
<Project>{3e10a5fa-e8da-48b1-ad44-6a5b6cb7750f}</Project> |
|||
<Name>Perspex.Themes.Default</Name> |
|||
</ProjectReference> |
|||
<ProjectReference Include="..\..\src\Windows\Perspex.Direct2D1\Perspex.Direct2D1.csproj"> |
|||
<Project>{3e908f67-5543-4879-a1dc-08eace79b3cd}</Project> |
|||
<Name>Perspex.Direct2D1</Name> |
|||
</ProjectReference> |
|||
<ProjectReference Include="..\..\src\Windows\Perspex.Win32\Perspex.Win32.csproj"> |
|||
<Project>{811a76cf-1cf6-440f-963b-bbe31bd72a82}</Project> |
|||
<Name>Perspex.Win32</Name> |
|||
</ProjectReference> |
|||
</ItemGroup> |
|||
<Import Project="$(MSBuildToolsPath)\Microsoft.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,22 @@ |
|||
<Window xmlns="https://github.com/perspex"> |
|||
<StackPanel Orientation="Horizontal"> |
|||
<StackPanel Margin="18" Gap="4" Width="200"> |
|||
<TextBlock FontSize="16" Text="Simple Bindings"/> |
|||
<TextBox Watermark="Two Way" UseFloatingWatermark="True" Text="{Binding StringValue}"/> |
|||
<TextBox Watermark="One Way" UseFloatingWatermark="True" Text="{Binding StringValue, Mode=OneWay}"/> |
|||
<TextBox Watermark="One Time" UseFloatingWatermark="True" Text="{Binding StringValue, Mode=OneTime}"/> |
|||
<TextBox Watermark="One Way To Source" UseFloatingWatermark="True" Text="{Binding StringValue, Mode=OneWayToSource}"/> |
|||
</StackPanel> |
|||
<StackPanel Margin="18" Gap="4" Width="200"> |
|||
<TextBlock FontSize="16" Text="Collection Bindings"/> |
|||
<TextBox Watermark="Items[1].StringValue" UseFloatingWatermark="True" Text="{Binding Items[1].StringValue}"/> |
|||
<Button Command="{Binding ShuffleItems}">Shuffle</Button> |
|||
</StackPanel> |
|||
<StackPanel Margin="18" Gap="4" Width="200"> |
|||
<TextBlock FontSize="16" Text="Negated Bindings"/> |
|||
<TextBox Watermark="Boolean String" UseFloatingWatermark="True" Text="{Binding BooleanString}"/> |
|||
<CheckBox IsChecked="{Binding !BooleanString}">!BooleanString</CheckBox> |
|||
<CheckBox IsChecked="{Binding !!BooleanString}">!!BooleanString</CheckBox> |
|||
</StackPanel> |
|||
</StackPanel> |
|||
</Window> |
|||
@ -0,0 +1,21 @@ |
|||
using BindingTest.ViewModels; |
|||
using Perspex.Controls; |
|||
using Perspex.Markup.Xaml; |
|||
|
|||
namespace BindingTest |
|||
{ |
|||
public class MainWindow : Window |
|||
{ |
|||
public MainWindow() |
|||
{ |
|||
this.InitializeComponent(); |
|||
this.DataContext = new MainWindowViewModel(); |
|||
App.AttachDevTools(this); |
|||
} |
|||
|
|||
private void InitializeComponent() |
|||
{ |
|||
PerspexXamlLoader.Load(this); |
|||
} |
|||
} |
|||
} |
|||
@ -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("BindingTest")] |
|||
[assembly: AssemblyDescription("")] |
|||
[assembly: AssemblyConfiguration("")] |
|||
[assembly: AssemblyCompany("")] |
|||
[assembly: AssemblyProduct("BindingTest")] |
|||
[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("08b3e6b9-1cd5-443c-9f61-6d49d1c5f162")] |
|||
|
|||
// 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,44 @@ |
|||
using System; |
|||
using System.Collections.ObjectModel; |
|||
using ReactiveUI; |
|||
|
|||
namespace BindingTest.ViewModels |
|||
{ |
|||
public class MainWindowViewModel : ReactiveObject |
|||
{ |
|||
private string _booleanString = "True"; |
|||
private string _stringValue = "Simple Binding"; |
|||
|
|||
public MainWindowViewModel() |
|||
{ |
|||
Items = new ObservableCollection<TestItem> |
|||
{ |
|||
new TestItem { StringValue = "Foo" }, |
|||
new TestItem { StringValue = "Bar" }, |
|||
new TestItem { StringValue = "Baz" }, |
|||
}; |
|||
|
|||
ShuffleItems = ReactiveCommand.Create(); |
|||
ShuffleItems.Subscribe(_ => |
|||
{ |
|||
var r = new Random(); |
|||
Items.Move(r.Next(Items.Count), 1); |
|||
}); |
|||
} |
|||
|
|||
public ObservableCollection<TestItem> Items { get; } |
|||
public ReactiveCommand<object> ShuffleItems { get; } |
|||
|
|||
public string BooleanString |
|||
{ |
|||
get { return _booleanString; } |
|||
set { this.RaiseAndSetIfChanged(ref _booleanString, value); } |
|||
} |
|||
|
|||
public string StringValue |
|||
{ |
|||
get { return _stringValue; } |
|||
set { this.RaiseAndSetIfChanged(ref _stringValue, value); } |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
using ReactiveUI; |
|||
|
|||
namespace BindingTest.ViewModels |
|||
{ |
|||
public class TestItem : ReactiveObject |
|||
{ |
|||
private string stringValue = "String Value"; |
|||
|
|||
public string StringValue |
|||
{ |
|||
get { return stringValue; } |
|||
set { this.RaiseAndSetIfChanged(ref this.stringValue, value); } |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
<?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="Serilog" version="1.5.9" targetFramework="net46" /> |
|||
<package id="Splat" version="1.6.2" targetFramework="net46" /> |
|||
</packages> |
|||
@ -0,0 +1,58 @@ |
|||
// 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.Collections.Generic; |
|||
|
|||
namespace XamlTestApplication.ViewModels |
|||
{ |
|||
public class MainWindowViewModel |
|||
{ |
|||
public MainWindowViewModel() |
|||
{ |
|||
Items = new List<TestItem>(); |
|||
|
|||
for (int i = 0; i < 10; ++i) |
|||
{ |
|||
Items.Add(new TestItem($"Item {i}", $"Item {i} Value")); |
|||
} |
|||
|
|||
Nodes = new List<TestNode> |
|||
{ |
|||
new TestNode |
|||
{ |
|||
Header = "Root", |
|||
SubHeader = "Root Item", |
|||
Children = new[] |
|||
{ |
|||
new TestNode |
|||
{ |
|||
Header = "Child 1", |
|||
SubHeader = "Child 1 Value", |
|||
}, |
|||
new TestNode |
|||
{ |
|||
Header = "Child 2", |
|||
SubHeader = "Child 2 Value", |
|||
Children = new[] |
|||
{ |
|||
new TestNode |
|||
{ |
|||
Header = "Grandchild", |
|||
SubHeader = "Grandchild Value", |
|||
}, |
|||
new TestNode |
|||
{ |
|||
Header = "Grandmaster Flash", |
|||
SubHeader = "White Lines", |
|||
}, |
|||
} |
|||
}, |
|||
} |
|||
} |
|||
}; |
|||
} |
|||
|
|||
public List<TestItem> Items { get; } |
|||
public List<TestNode> Nodes { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
// Copyright (c) The Perspex Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
namespace XamlTestApplication.ViewModels |
|||
{ |
|||
public class TestItem |
|||
{ |
|||
public TestItem(string header, string subheader) |
|||
{ |
|||
Header = header; |
|||
SubHeader = subheader; |
|||
} |
|||
|
|||
public string Header { get; } |
|||
public string SubHeader { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
// 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.Collections.Generic; |
|||
|
|||
namespace XamlTestApplication.ViewModels |
|||
{ |
|||
public class TestNode |
|||
{ |
|||
public string Header { get; set; } |
|||
public string SubHeader { get; set; } |
|||
public IEnumerable<TestNode> Children { get; set; } |
|||
} |
|||
} |
|||
@ -1,7 +1,7 @@ |
|||
// Copyright (c) The Perspex Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
namespace Perspex.Markup.Xaml.DataBinding |
|||
namespace Perspex.Markup.Xaml.Binding |
|||
{ |
|||
public class TargetBindingEndpoint |
|||
{ |
|||
@ -0,0 +1,70 @@ |
|||
// 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.Linq; |
|||
using System.Reactive.Subjects; |
|||
using OmniXaml.TypeConversion; |
|||
using Perspex.Controls; |
|||
using Perspex.Markup.Binding; |
|||
|
|||
namespace Perspex.Markup.Xaml.Binding |
|||
{ |
|||
public class XamlBinding |
|||
{ |
|||
private readonly ITypeConverterProvider _typeConverterProvider; |
|||
|
|||
public XamlBinding() |
|||
{ |
|||
} |
|||
|
|||
public XamlBinding(ITypeConverterProvider typeConverterProvider) |
|||
{ |
|||
_typeConverterProvider = typeConverterProvider; |
|||
} |
|||
|
|||
public string SourcePropertyPath { get; set; } |
|||
|
|||
public BindingMode BindingMode { get; set; } |
|||
|
|||
public void Bind(IObservablePropertyBag instance, PerspexProperty property) |
|||
{ |
|||
var subject = new ExpressionSubject(CreateExpressionObserver(instance)); |
|||
Bind(instance, property, subject); |
|||
} |
|||
|
|||
public ExpressionObserver CreateExpressionObserver(IObservablePropertyBag instance) |
|||
{ |
|||
var result = new ExpressionObserver(null, SourcePropertyPath); |
|||
var dataContext = instance.GetObservable(Control.DataContextProperty); |
|||
dataContext.Subscribe(x => result.Root = x); |
|||
return result; |
|||
} |
|||
|
|||
internal void Bind(IObservablePropertyBag target, PerspexProperty property, ISubject<object> subject) |
|||
{ |
|||
var mode = BindingMode == BindingMode.Default ? |
|||
property.DefaultBindingMode : BindingMode; |
|||
|
|||
switch (mode) |
|||
{ |
|||
case BindingMode.Default: |
|||
case BindingMode.OneWay: |
|||
target.Bind(property, subject); |
|||
break; |
|||
case BindingMode.TwoWay: |
|||
target.BindTwoWay(property, subject); |
|||
break; |
|||
case BindingMode.OneTime: |
|||
target.GetObservable(Control.DataContextProperty).Subscribe(dataContext => |
|||
{ |
|||
subject.Take(1).Subscribe(x => target.SetValue(property, x)); |
|||
}); |
|||
break; |
|||
case BindingMode.OneWayToSource: |
|||
target.GetObservable(property).Subscribe(subject); |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
// 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.Controls; |
|||
|
|||
namespace Perspex.Markup.Xaml.Binding |
|||
{ |
|||
public class XamlBindingDefinition |
|||
{ |
|||
public XamlBindingDefinition( |
|||
string sourcePropertyPath, |
|||
BindingMode bindingMode) |
|||
{ |
|||
SourcePropertyPath = sourcePropertyPath; |
|||
BindingMode = bindingMode; |
|||
} |
|||
|
|||
public string SourcePropertyPath { get; } |
|||
public BindingMode BindingMode { get; } |
|||
} |
|||
} |
|||
@ -1,109 +0,0 @@ |
|||
// 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.ComponentModel; |
|||
using System.Linq; |
|||
using System.Reactive.Linq; |
|||
using System.Reflection; |
|||
using Glass; |
|||
|
|||
namespace Perspex.Markup.Xaml.DataBinding.ChangeTracking |
|||
{ |
|||
public class ObservablePropertyBranch |
|||
{ |
|||
private readonly object _instance; |
|||
private readonly PropertyPath _propertyPath; |
|||
private readonly PropertyMountPoint _mountPoint; |
|||
|
|||
public ObservablePropertyBranch(object instance, PropertyPath propertyPath) |
|||
{ |
|||
Guard.ThrowIfNull(instance, nameof(instance)); |
|||
Guard.ThrowIfNull(propertyPath, nameof(propertyPath)); |
|||
|
|||
_instance = instance; |
|||
_propertyPath = propertyPath; |
|||
_mountPoint = new PropertyMountPoint(instance, propertyPath); |
|||
var properties = GetPropertiesThatRaiseNotifications(); |
|||
Values = CreateUnifiedObservableFromNodes(properties); |
|||
} |
|||
|
|||
public IObservable<object> Values { get; private set; } |
|||
|
|||
private IObservable<object> CreateUnifiedObservableFromNodes(IEnumerable<PropertyDefinition> subscriptions) |
|||
{ |
|||
return subscriptions.Select(GetObservableFromProperty).Merge(); |
|||
} |
|||
|
|||
private IObservable<object> GetObservableFromProperty(PropertyDefinition subscription) |
|||
{ |
|||
return Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>( |
|||
parentOnPropertyChanged => subscription.Parent.PropertyChanged += parentOnPropertyChanged, |
|||
parentOnPropertyChanged => subscription.Parent.PropertyChanged -= parentOnPropertyChanged) |
|||
.Where(pattern => pattern.EventArgs.PropertyName == subscription.PropertyName) |
|||
.Select(pattern => _mountPoint.Value); |
|||
} |
|||
|
|||
private IEnumerable<PropertyDefinition> GetPropertiesThatRaiseNotifications() |
|||
{ |
|||
return GetSubscriptionsRecursive(_instance, _propertyPath, 0); |
|||
} |
|||
|
|||
private IEnumerable<PropertyDefinition> GetSubscriptionsRecursive(object current, PropertyPath propertyPath, int i) |
|||
{ |
|||
var subscriptions = new List<PropertyDefinition>(); |
|||
var inpc = current as INotifyPropertyChanged; |
|||
|
|||
if (inpc == null) |
|||
{ |
|||
return subscriptions; |
|||
} |
|||
|
|||
var nextPropertyName = propertyPath.Chunks[i]; |
|||
subscriptions.Add(new PropertyDefinition(inpc, nextPropertyName)); |
|||
|
|||
if (i < _propertyPath.Chunks.Length) |
|||
{ |
|||
var currentObjectTypeInfo = current.GetType().GetTypeInfo(); |
|||
var nextProperty = currentObjectTypeInfo.GetDeclaredProperty(nextPropertyName); |
|||
var nextInstance = nextProperty.GetValue(current); |
|||
|
|||
if (i < _propertyPath.Chunks.Length - 1) |
|||
{ |
|||
subscriptions.AddRange(GetSubscriptionsRecursive(nextInstance, propertyPath, i + 1)); |
|||
} |
|||
} |
|||
|
|||
return subscriptions; |
|||
} |
|||
|
|||
public object Value |
|||
{ |
|||
get |
|||
{ |
|||
return _mountPoint.Value; |
|||
} |
|||
|
|||
set |
|||
{ |
|||
_mountPoint.Value = value; |
|||
} |
|||
} |
|||
|
|||
public Type Type => _mountPoint.ProperyType; |
|||
|
|||
private class PropertyDefinition |
|||
{ |
|||
public PropertyDefinition(INotifyPropertyChanged parent, string propertyName) |
|||
{ |
|||
Parent = parent; |
|||
PropertyName = propertyName; |
|||
} |
|||
|
|||
public INotifyPropertyChanged Parent { get; } |
|||
|
|||
public string PropertyName { get; } |
|||
} |
|||
} |
|||
} |
|||
@ -1,52 +0,0 @@ |
|||
// 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.Reflection; |
|||
using Glass; |
|||
|
|||
namespace Perspex.Markup.Xaml.DataBinding.ChangeTracking |
|||
{ |
|||
public class PropertyMountPoint |
|||
{ |
|||
private readonly TargettedProperty _referencedTargettedProperty; |
|||
|
|||
public PropertyMountPoint(object origin, PropertyPath propertyPath) |
|||
{ |
|||
Guard.ThrowIfNull(origin, nameof(origin)); |
|||
Guard.ThrowIfNull(propertyPath, nameof(propertyPath)); |
|||
|
|||
_referencedTargettedProperty = GetReferencedPropertyInfo(origin, propertyPath, 0); |
|||
} |
|||
|
|||
private static TargettedProperty GetReferencedPropertyInfo(object current, PropertyPath propertyPath, int level) |
|||
{ |
|||
var typeInfo = current.GetType().GetTypeInfo(); |
|||
var leftPropertyInfo = typeInfo.GetDeclaredProperty(propertyPath.Chunks[level]); |
|||
|
|||
if (level == propertyPath.Chunks.Length - 1) |
|||
{ |
|||
return new TargettedProperty(current, leftPropertyInfo); |
|||
} |
|||
|
|||
var nextInstance = leftPropertyInfo.GetValue(current); |
|||
|
|||
return GetReferencedPropertyInfo(nextInstance, propertyPath, level + 1); |
|||
} |
|||
|
|||
public object Value |
|||
{ |
|||
get |
|||
{ |
|||
return _referencedTargettedProperty.Value; |
|||
} |
|||
|
|||
set |
|||
{ |
|||
_referencedTargettedProperty.Value = value; |
|||
} |
|||
} |
|||
|
|||
public Type ProperyType => _referencedTargettedProperty.PropertyType; |
|||
} |
|||
} |
|||
@ -1,36 +0,0 @@ |
|||
// Copyright (c) The Perspex Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
namespace Perspex.Markup.Xaml.DataBinding.ChangeTracking |
|||
{ |
|||
public class PropertyPath |
|||
{ |
|||
private string[] _chunks; |
|||
|
|||
private PropertyPath(PropertyPath propertyPath) |
|||
{ |
|||
_chunks = propertyPath.Chunks; |
|||
} |
|||
|
|||
public PropertyPath(string path) |
|||
{ |
|||
_chunks = path.Split('.'); |
|||
} |
|||
|
|||
public string[] Chunks |
|||
{ |
|||
get { return _chunks; } |
|||
set { _chunks = value; } |
|||
} |
|||
|
|||
public PropertyPath Clone() |
|||
{ |
|||
return new PropertyPath(this); |
|||
} |
|||
|
|||
public override string ToString() |
|||
{ |
|||
return string.Join(".", _chunks); |
|||
} |
|||
} |
|||
} |
|||
@ -1,41 +0,0 @@ |
|||
// 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.Reflection; |
|||
using Glass; |
|||
|
|||
namespace Perspex.Markup.Xaml.DataBinding.ChangeTracking |
|||
{ |
|||
internal class TargettedProperty |
|||
{ |
|||
private readonly object _instance; |
|||
private readonly PropertyInfo _propertyInfo; |
|||
|
|||
public TargettedProperty(object instance, PropertyInfo propertyInfo) |
|||
{ |
|||
Guard.ThrowIfNull(instance, nameof(instance)); |
|||
Guard.ThrowIfNull(propertyInfo, nameof(propertyInfo)); |
|||
|
|||
_instance = instance; |
|||
_propertyInfo = propertyInfo; |
|||
} |
|||
|
|||
public object Value |
|||
{ |
|||
get |
|||
{ |
|||
return _propertyInfo.GetValue(_instance); |
|||
} |
|||
|
|||
set |
|||
{ |
|||
_propertyInfo.SetValue(_instance, value); |
|||
} |
|||
} |
|||
|
|||
public Type PropertyType => _propertyInfo.PropertyType; |
|||
|
|||
public string Name => _propertyInfo.Name; |
|||
} |
|||
} |
|||
@ -1,165 +0,0 @@ |
|||
// 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.Globalization; |
|||
using System.Reactive.Linq; |
|||
using System.Reflection; |
|||
using Glass; |
|||
using OmniXaml.TypeConversion; |
|||
using Perspex.Markup.Xaml.DataBinding.ChangeTracking; |
|||
|
|||
namespace Perspex.Markup.Xaml.DataBinding |
|||
{ |
|||
public class DataContextChangeSynchronizer |
|||
{ |
|||
private readonly BindingTarget _bindingTarget; |
|||
private readonly ITypeConverter _targetPropertyTypeConverter; |
|||
private readonly TargetBindingEndpoint _bindingEndpoint; |
|||
private readonly ObservablePropertyBranch _sourceEndpoint; |
|||
|
|||
public DataContextChangeSynchronizer(BindingSource bindingSource, BindingTarget bindingTarget, ITypeConverterProvider typeConverterProvider) |
|||
{ |
|||
_bindingTarget = bindingTarget; |
|||
Guard.ThrowIfNull(bindingTarget.Object, nameof(bindingTarget.Object)); |
|||
Guard.ThrowIfNull(bindingTarget.Property, nameof(bindingTarget.Property)); |
|||
Guard.ThrowIfNull(bindingSource.SourcePropertyPath, nameof(bindingSource.SourcePropertyPath)); |
|||
Guard.ThrowIfNull(bindingSource.Source, nameof(bindingSource.Source)); |
|||
Guard.ThrowIfNull(typeConverterProvider, nameof(typeConverterProvider)); |
|||
|
|||
_bindingEndpoint = new TargetBindingEndpoint(bindingTarget.Object, bindingTarget.Property); |
|||
_sourceEndpoint = new ObservablePropertyBranch(bindingSource.Source, bindingSource.SourcePropertyPath); |
|||
_targetPropertyTypeConverter = typeConverterProvider.GetTypeConverter(bindingTarget.Property.PropertyType); |
|||
} |
|||
|
|||
public class BindingTarget |
|||
{ |
|||
private readonly PerspexObject _obj; |
|||
private readonly PerspexProperty _property; |
|||
|
|||
public BindingTarget(PerspexObject @object, PerspexProperty property) |
|||
{ |
|||
_obj = @object; |
|||
_property = property; |
|||
} |
|||
|
|||
public PerspexObject Object => _obj; |
|||
|
|||
public PerspexProperty Property => _property; |
|||
|
|||
public object Value |
|||
{ |
|||
get { return _obj.GetValue(_property); } |
|||
set { _obj.SetValue(_property, value); } |
|||
} |
|||
} |
|||
|
|||
public class BindingSource |
|||
{ |
|||
private readonly PropertyPath _sourcePropertyPath; |
|||
private readonly object _source; |
|||
|
|||
public BindingSource(PropertyPath sourcePropertyPath, object source) |
|||
{ |
|||
_sourcePropertyPath = sourcePropertyPath; |
|||
_source = source; |
|||
} |
|||
|
|||
public PropertyPath SourcePropertyPath => _sourcePropertyPath; |
|||
|
|||
public object Source => _source; |
|||
} |
|||
|
|||
public void StartUpdatingTargetWhenSourceChanges() |
|||
{ |
|||
// TODO: commenting out this line will make the existing value to be skipped from the SourceValues. This is not supposed to happen. Is it?
|
|||
_bindingTarget.Value = ConvertedValue(_sourceEndpoint.Value, _bindingTarget.Property.PropertyType); |
|||
|
|||
// We use the native Bind method from PerspexObject to subscribe to the SourceValues observable
|
|||
_bindingTarget.Object.Bind(_bindingTarget.Property, SourceValues); |
|||
} |
|||
|
|||
public void StartUpdatingSourceWhenTargetChanges() |
|||
{ |
|||
// We subscribe to the TargetValues and each time we have a new value, we update the source with it
|
|||
TargetValues.Subscribe(newValue => _sourceEndpoint.Value = newValue); |
|||
} |
|||
|
|||
private IObservable<object> SourceValues |
|||
{ |
|||
get |
|||
{ |
|||
return _sourceEndpoint.Values.Select(originalValue => ConvertedValue(originalValue, _bindingTarget.Property.PropertyType)); |
|||
} |
|||
} |
|||
|
|||
private IObservable<object> TargetValues |
|||
{ |
|||
get |
|||
{ |
|||
return _bindingEndpoint.Object |
|||
.GetObservable(_bindingEndpoint.Property).Select(o => ConvertedValue(o, _sourceEndpoint.Type)); |
|||
} |
|||
} |
|||
|
|||
private bool CanAssignWithoutConversion |
|||
{ |
|||
get |
|||
{ |
|||
var sourceTypeInfo = _sourceEndpoint.Type.GetTypeInfo(); |
|||
var targetTypeInfo = _bindingEndpoint.Property.PropertyType.GetTypeInfo(); |
|||
var compatible = targetTypeInfo.IsAssignableFrom(sourceTypeInfo); |
|||
return compatible; |
|||
} |
|||
} |
|||
|
|||
private object ConvertedValue(object originalValue, Type propertyType) |
|||
{ |
|||
object converted; |
|||
if (TryConvert(originalValue, propertyType, out converted)) |
|||
{ |
|||
return converted; |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
private bool TryConvert(object originalValue, Type targetType, out object finalValue) |
|||
{ |
|||
if (originalValue != null) |
|||
{ |
|||
if (CanAssignWithoutConversion) |
|||
{ |
|||
finalValue = originalValue; |
|||
return true; |
|||
} |
|||
|
|||
if (_targetPropertyTypeConverter != null) |
|||
{ |
|||
if (_targetPropertyTypeConverter.CanConvertTo(null, targetType)) |
|||
{ |
|||
object convertedValue = _targetPropertyTypeConverter.ConvertTo( |
|||
null, |
|||
CultureInfo.InvariantCulture, |
|||
originalValue, |
|||
targetType); |
|||
|
|||
if (convertedValue != null) |
|||
{ |
|||
finalValue = convertedValue; |
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
finalValue = null; |
|||
return true; |
|||
} |
|||
|
|||
finalValue = null; |
|||
return false; |
|||
} |
|||
} |
|||
} |
|||
@ -1,16 +0,0 @@ |
|||
// 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.Collections.Generic; |
|||
|
|||
namespace Perspex.Markup.Xaml.DataBinding |
|||
{ |
|||
public interface IPerspexPropertyBinder |
|||
{ |
|||
XamlBinding GetBinding(PerspexObject po, PerspexProperty pp); |
|||
|
|||
IEnumerable<XamlBinding> GetBindings(PerspexObject source); |
|||
|
|||
XamlBinding Create(XamlBindingDefinition xamlBinding); |
|||
} |
|||
} |
|||
@ -1,59 +0,0 @@ |
|||
// 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 OmniXaml.TypeConversion; |
|||
|
|||
namespace Perspex.Markup.Xaml.DataBinding |
|||
{ |
|||
public class PerspexPropertyBinder : IPerspexPropertyBinder |
|||
{ |
|||
private readonly ITypeConverterProvider _typeConverterProvider; |
|||
|
|||
private readonly HashSet<XamlBinding> _bindings; |
|||
|
|||
public PerspexPropertyBinder(ITypeConverterProvider typeConverterProvider) |
|||
{ |
|||
_typeConverterProvider = typeConverterProvider; |
|||
_bindings = new HashSet<XamlBinding>(); |
|||
} |
|||
|
|||
public XamlBinding GetBinding(PerspexObject po, PerspexProperty pp) |
|||
{ |
|||
return _bindings.First(xamlBinding => xamlBinding.Target == po && xamlBinding.TargetProperty == pp); |
|||
} |
|||
|
|||
public IEnumerable<XamlBinding> GetBindings(PerspexObject source) |
|||
{ |
|||
return from binding in _bindings |
|||
where binding.Target == source |
|||
select binding; |
|||
} |
|||
|
|||
public XamlBinding Create(XamlBindingDefinition xamlBinding) |
|||
{ |
|||
if (xamlBinding.Target == null) |
|||
{ |
|||
throw new InvalidOperationException(); |
|||
} |
|||
|
|||
if (xamlBinding.TargetProperty == null) |
|||
{ |
|||
throw new InvalidOperationException(); |
|||
} |
|||
|
|||
var binding = new XamlBinding(_typeConverterProvider) |
|||
{ |
|||
BindingMode = xamlBinding.BindingMode, |
|||
SourcePropertyPath = xamlBinding.SourcePropertyPath, |
|||
Target = xamlBinding.Target, |
|||
TargetProperty = xamlBinding.TargetProperty |
|||
}; |
|||
|
|||
_bindings.Add(binding); |
|||
return binding; |
|||
} |
|||
} |
|||
} |
|||
@ -1,65 +0,0 @@ |
|||
// 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.Diagnostics; |
|||
using OmniXaml.TypeConversion; |
|||
using Perspex.Markup.Xaml.DataBinding.ChangeTracking; |
|||
|
|||
namespace Perspex.Markup.Xaml.DataBinding |
|||
{ |
|||
public class XamlBinding |
|||
{ |
|||
private readonly ITypeConverterProvider _typeConverterProvider; |
|||
private DataContextChangeSynchronizer _changeSynchronizer; |
|||
|
|||
public XamlBinding(ITypeConverterProvider typeConverterProvider) |
|||
{ |
|||
_typeConverterProvider = typeConverterProvider; |
|||
} |
|||
|
|||
public PerspexObject Target { get; set; } |
|||
|
|||
public PerspexProperty TargetProperty { get; set; } |
|||
|
|||
public PropertyPath SourcePropertyPath { get; set; } |
|||
|
|||
public BindingMode BindingMode { get; set; } |
|||
|
|||
public void BindToDataContext(object dataContext) |
|||
{ |
|||
if (dataContext == null) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
try |
|||
{ |
|||
var bindingSource = new DataContextChangeSynchronizer.BindingSource(SourcePropertyPath, dataContext); |
|||
var bindingTarget = new DataContextChangeSynchronizer.BindingTarget(Target, TargetProperty); |
|||
|
|||
_changeSynchronizer = new DataContextChangeSynchronizer(bindingSource, bindingTarget, _typeConverterProvider); |
|||
|
|||
if (BindingMode == BindingMode.TwoWay) |
|||
{ |
|||
_changeSynchronizer.StartUpdatingTargetWhenSourceChanges(); |
|||
_changeSynchronizer.StartUpdatingSourceWhenTargetChanges(); |
|||
} |
|||
|
|||
if (BindingMode == BindingMode.OneWay) |
|||
{ |
|||
_changeSynchronizer.StartUpdatingTargetWhenSourceChanges(); |
|||
} |
|||
|
|||
if (BindingMode == BindingMode.OneWayToSource) |
|||
{ |
|||
_changeSynchronizer.StartUpdatingSourceWhenTargetChanges(); |
|||
} |
|||
} |
|||
catch (Exception e) |
|||
{ |
|||
Debug.WriteLine(e); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,32 +0,0 @@ |
|||
// 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.Controls; |
|||
using Perspex.Markup.Xaml.DataBinding.ChangeTracking; |
|||
|
|||
namespace Perspex.Markup.Xaml.DataBinding |
|||
{ |
|||
public class XamlBindingDefinition |
|||
{ |
|||
private readonly PropertyPath _sourcePropertyPath; |
|||
private readonly BindingMode _bindingMode; |
|||
private readonly Control _target; |
|||
private readonly PerspexProperty _targetProperty; |
|||
|
|||
public XamlBindingDefinition(Control target, PerspexProperty targetProperty, PropertyPath sourcePropertyPath, BindingMode bindingMode) |
|||
{ |
|||
_target = target; |
|||
_targetProperty = targetProperty; |
|||
_sourcePropertyPath = sourcePropertyPath; |
|||
_bindingMode = bindingMode; |
|||
} |
|||
|
|||
public Control Target => _target; |
|||
|
|||
public PerspexProperty TargetProperty => _targetProperty; |
|||
|
|||
public PropertyPath SourcePropertyPath => _sourcePropertyPath; |
|||
|
|||
public BindingMode BindingMode => _bindingMode; |
|||
} |
|||
} |
|||
@ -1 +1 @@ |
|||
Subproject commit 49e6ec001f5873cf2290e0bc1f6f06ca9b9cf808 |
|||
Subproject commit 42b0e3b6efc0905457120752be38a6000898fffa |
|||
@ -0,0 +1,53 @@ |
|||
// 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; |
|||
using System.Reactive.Linq; |
|||
using Perspex.Controls; |
|||
using Perspex.Controls.Templates; |
|||
using Perspex.Markup.Binding; |
|||
using Perspex.Markup.Xaml.Binding; |
|||
|
|||
namespace Perspex.Markup.Xaml.Templates |
|||
{ |
|||
public class TreeDataTemplate : ITreeDataTemplate |
|||
{ |
|||
public Type DataType { get; set; } |
|||
public TemplateContent Content { get; set; } |
|||
public XamlBindingDefinition ItemsSource { get; set; } |
|||
|
|||
public bool Match(object data) |
|||
{ |
|||
if (DataType == null) |
|||
{ |
|||
throw new InvalidOperationException("DataTemplate must have a DataType."); |
|||
} |
|||
|
|||
return DataType == data.GetType(); |
|||
} |
|||
|
|||
public IEnumerable ItemsSelector(object item) |
|||
{ |
|||
if (ItemsSource != null) |
|||
{ |
|||
var obs = new ExpressionObserver(item, ItemsSource.SourcePropertyPath); |
|||
return obs.Take(1).Wait().Value as IEnumerable; |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
public bool IsExpanded(object item) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
public IControl Build(object data) |
|||
{ |
|||
var visualTreeForItem = Content.Load(); |
|||
visualTreeForItem.DataContext = data; |
|||
return visualTreeForItem; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,102 @@ |
|||
// 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.Subjects; |
|||
|
|||
namespace Perspex.Markup.Binding |
|||
{ |
|||
internal 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) |
|||
{ |
|||
SubscribeAndUpdate(_target); |
|||
} |
|||
else |
|||
{ |
|||
CurrentValue = ExpressionValue.None; |
|||
} |
|||
|
|||
if (Next != null) |
|||
{ |
|||
Next.Target = CurrentValue.Value; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public ExpressionValue CurrentValue |
|||
{ |
|||
get |
|||
{ |
|||
return _value; |
|||
} |
|||
|
|||
set |
|||
{ |
|||
_value = value; |
|||
|
|||
if (Next != null) |
|||
{ |
|||
Next.Target = value.Value; |
|||
} |
|||
|
|||
if (_subject != null) |
|||
{ |
|||
_subject.OnNext(value); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public virtual bool SetValue(object value) |
|||
{ |
|||
return Next?.SetValue(value) ?? false; |
|||
} |
|||
|
|||
public virtual 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 virtual void SubscribeAndUpdate(object target) |
|||
{ |
|||
CurrentValue = new ExpressionValue(target); |
|||
} |
|||
|
|||
protected virtual void Unsubscribe(object target) |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -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 System; |
|||
using Perspex.Markup.Binding.Parsers; |
|||
|
|||
namespace Perspex.Markup.Binding |
|||
{ |
|||
internal static class ExpressionNodeBuilder |
|||
{ |
|||
public static ExpressionNode Build(string expression) |
|||
{ |
|||
if (string.IsNullOrWhiteSpace(expression)) |
|||
{ |
|||
throw new ArgumentException("'expression' may not be empty."); |
|||
} |
|||
|
|||
var reader = new Reader(expression); |
|||
var node = ExpressionParser.Parse(reader); |
|||
|
|||
if (!reader.End) |
|||
{ |
|||
throw new ExpressionParseException(reader, "Expected end of expression."); |
|||
} |
|||
|
|||
return node; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,115 @@ |
|||
// 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.Disposables; |
|||
|
|||
namespace Perspex.Markup.Binding |
|||
{ |
|||
/// <summary>
|
|||
/// Observes and sets the value of an expression on an object.
|
|||
/// </summary>
|
|||
public class ExpressionObserver : ObservableBase<ExpressionValue>, IDescription |
|||
{ |
|||
private object _root; |
|||
private int _count; |
|||
private ExpressionNode _node; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
|
|||
/// </summary>
|
|||
/// <param name="root">The root object.</param>
|
|||
/// <param name="expression">The expression.</param>
|
|||
public ExpressionObserver(object root, string expression) |
|||
{ |
|||
_root = root; |
|||
_node = ExpressionNodeBuilder.Build(expression); |
|||
Expression = expression; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Attempts to set the value of a property expression.
|
|||
/// </summary>
|
|||
/// <param name="value">The value to set.</param>
|
|||
/// <returns>
|
|||
/// True if the value could be set; false if the expression does not evaluate to a
|
|||
/// property.
|
|||
/// </returns>
|
|||
public bool SetValue(object value) |
|||
{ |
|||
IncrementCount(); |
|||
|
|||
try |
|||
{ |
|||
return _node.SetValue(value); |
|||
} |
|||
finally |
|||
{ |
|||
DecrementCount(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the expression being observed.
|
|||
/// </summary>
|
|||
public string Expression { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the root object that the expression is being observed on.
|
|||
/// </summary>
|
|||
public object Root |
|||
{ |
|||
get |
|||
{ |
|||
return _root; |
|||
} |
|||
|
|||
set |
|||
{ |
|||
if (_root != value) |
|||
{ |
|||
_root = value; |
|||
|
|||
if (_count > 0) |
|||
{ |
|||
_node.Target = _root; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
string IDescription.Description => Expression; |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override IDisposable SubscribeCore(IObserver<ExpressionValue> observer) |
|||
{ |
|||
IncrementCount(); |
|||
|
|||
var subscription = _node.Subscribe(observer); |
|||
|
|||
return Disposable.Create(() => |
|||
{ |
|||
DecrementCount(); |
|||
subscription.Dispose(); |
|||
}); |
|||
} |
|||
|
|||
private void IncrementCount() |
|||
{ |
|||
if (_count++ == 0) |
|||
{ |
|||
_node.Target = Root; |
|||
} |
|||
} |
|||
|
|||
private void DecrementCount() |
|||
{ |
|||
if (--_count == 0) |
|||
{ |
|||
_node.Target = null; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
// 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 Perspex.Markup.Binding.Parsers; |
|||
|
|||
namespace Perspex.Markup.Binding |
|||
{ |
|||
public class ExpressionParseException : Exception |
|||
{ |
|||
internal ExpressionParseException(int column, string message) |
|||
: base(message) |
|||
{ |
|||
Column = column; |
|||
} |
|||
|
|||
internal ExpressionParseException(Reader r, string message) |
|||
: this(r.Position, message) |
|||
{ |
|||
} |
|||
|
|||
public int Column { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
// 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.Linq; |
|||
using System.Reactive.Subjects; |
|||
|
|||
namespace Perspex.Markup.Binding |
|||
{ |
|||
/// <summary>
|
|||
/// Turns an <see cref="ExpressionObserver"/> into a subject that can be bound two-ways.
|
|||
/// </summary>
|
|||
public class ExpressionSubject : ISubject<object>, IDescription |
|||
{ |
|||
private ExpressionObserver _inner; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ExpressionObserver"/> class.
|
|||
/// </summary>
|
|||
/// <param name="inner">The <see cref="ExpressionObserver"/>.</param>
|
|||
public ExpressionSubject(ExpressionObserver inner) |
|||
{ |
|||
_inner = inner; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
string IDescription.Description => _inner.Expression; |
|||
|
|||
/// <inheritdoc/>
|
|||
public void OnCompleted() |
|||
{ |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void OnError(Exception error) |
|||
{ |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void OnNext(object value) |
|||
{ |
|||
_inner.SetValue(value); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public IDisposable Subscribe(IObserver<object> observer) |
|||
{ |
|||
return _inner.Select(x => x.Value).Subscribe(observer); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
|
|||
namespace Perspex.Markup.Binding |
|||
{ |
|||
/// <summary>
|
|||
/// Holds the value for an <see cref="ExpressionObserver"/>.
|
|||
/// </summary>
|
|||
public struct ExpressionValue |
|||
{ |
|||
/// <summary>
|
|||
/// An <see cref="ExpressionValue"/> that has no value.
|
|||
/// </summary>
|
|||
public static readonly ExpressionValue None = new ExpressionValue(); |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ExpressionValue"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="value"></param>
|
|||
public ExpressionValue(object value) |
|||
{ |
|||
HasValue = true; |
|||
Value = value; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether the evaluated expression resulted in a value.
|
|||
/// </summary>
|
|||
public bool HasValue { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a the result of the expression.
|
|||
/// </summary>
|
|||
public object Value { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,106 @@ |
|||
// 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; |
|||
using System.Collections.Generic; |
|||
using System.Collections.Specialized; |
|||
using System.Linq; |
|||
using System.Reflection; |
|||
|
|||
namespace Perspex.Markup.Binding |
|||
{ |
|||
internal class IndexerNode : ExpressionNode |
|||
{ |
|||
private int[] _intArgs; |
|||
|
|||
public IndexerNode(IList<object> arguments) |
|||
{ |
|||
Arguments = arguments; |
|||
|
|||
var intArgs = Arguments.OfType<int>().ToArray(); |
|||
|
|||
if (intArgs.Length == arguments.Count) |
|||
{ |
|||
_intArgs = intArgs; |
|||
} |
|||
} |
|||
|
|||
public IList<object> Arguments { get; } |
|||
|
|||
protected override void SubscribeAndUpdate(object target) |
|||
{ |
|||
CurrentValue = GetValue(target); |
|||
|
|||
var incc = target as INotifyCollectionChanged; |
|||
|
|||
if (incc != null) |
|||
{ |
|||
incc.CollectionChanged += CollectionChanged; |
|||
} |
|||
} |
|||
|
|||
protected override void Unsubscribe(object target) |
|||
{ |
|||
var incc = target as INotifyCollectionChanged; |
|||
|
|||
if (incc != null) |
|||
{ |
|||
incc.CollectionChanged -= CollectionChanged; |
|||
} |
|||
} |
|||
|
|||
private void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) |
|||
{ |
|||
bool update = false; |
|||
|
|||
switch (e.Action) |
|||
{ |
|||
case NotifyCollectionChangedAction.Add: |
|||
update = _intArgs[0] >= e.NewStartingIndex; |
|||
break; |
|||
case NotifyCollectionChangedAction.Remove: |
|||
update = _intArgs[0] >= e.OldStartingIndex; |
|||
break; |
|||
case NotifyCollectionChangedAction.Replace: |
|||
update = _intArgs[0] >= e.NewStartingIndex && |
|||
_intArgs[0] < e.NewStartingIndex + e.NewItems.Count; |
|||
break; |
|||
case NotifyCollectionChangedAction.Move: |
|||
update = (_intArgs[0] >= e.NewStartingIndex && |
|||
_intArgs[0] < e.NewStartingIndex + e.NewItems.Count) || |
|||
(_intArgs[0] >= e.OldStartingIndex && |
|||
_intArgs[0] < e.OldStartingIndex + e.OldItems.Count); |
|||
break; |
|||
case NotifyCollectionChangedAction.Reset: |
|||
update = true; |
|||
break; |
|||
} |
|||
|
|||
if (update) |
|||
{ |
|||
CurrentValue = GetValue(sender); |
|||
} |
|||
} |
|||
|
|||
private ExpressionValue GetValue(object target) |
|||
{ |
|||
var typeInfo = target.GetType().GetTypeInfo(); |
|||
var list = target as IList; |
|||
|
|||
if (typeInfo.IsArray && _intArgs != null) |
|||
{ |
|||
return new ExpressionValue(((Array)target).GetValue(_intArgs)); |
|||
} |
|||
else if (target is IList && _intArgs?.Length == 1) |
|||
{ |
|||
if (_intArgs[0] < list.Count) |
|||
{ |
|||
return new ExpressionValue(list[_intArgs[0]]); |
|||
} |
|||
} |
|||
|
|||
return ExpressionValue.None; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
// 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.Globalization; |
|||
using System.Reactive.Linq; |
|||
|
|||
namespace Perspex.Markup.Binding |
|||
{ |
|||
internal class LogicalNotNode : ExpressionNode |
|||
{ |
|||
public override bool SetValue(object value) |
|||
{ |
|||
throw new NotSupportedException("Cannot set a negated binding."); |
|||
} |
|||
|
|||
public override IDisposable Subscribe(IObserver<ExpressionValue> observer) |
|||
{ |
|||
return Next.Select(x => Negate(x)).Subscribe(observer); |
|||
} |
|||
|
|||
private ExpressionValue Negate(ExpressionValue v) |
|||
{ |
|||
if (v.HasValue) |
|||
{ |
|||
try |
|||
{ |
|||
var boolean = Convert.ToBoolean(v.Value, CultureInfo.InvariantCulture); |
|||
return new ExpressionValue(!boolean); |
|||
} |
|||
catch |
|||
{ |
|||
// TODO: Maybe should log something here.
|
|||
} |
|||
} |
|||
|
|||
return ExpressionValue.None; |
|||
} |
|||
} |
|||
} |
|||
@ -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.Collections.Generic; |
|||
|
|||
namespace Perspex.Markup.Binding.Parsers |
|||
{ |
|||
internal static class ArgumentListParser |
|||
{ |
|||
public static IList<object> Parse(Reader r, char open, char close) |
|||
{ |
|||
if (r.Peek == open) |
|||
{ |
|||
var result = new List<object>(); |
|||
|
|||
r.Take(); |
|||
|
|||
while (!r.End) |
|||
{ |
|||
var literal = LiteralParser.Parse(r); |
|||
|
|||
if (literal != null) |
|||
{ |
|||
result.Add(literal); |
|||
} |
|||
else |
|||
{ |
|||
throw new ExpressionParseException(r, "Expected integer."); |
|||
} |
|||
|
|||
r.SkipWhitespace(); |
|||
|
|||
if (r.End) |
|||
{ |
|||
throw new ExpressionParseException(r, "Expected ','."); |
|||
} |
|||
else if (r.TakeIf(close)) |
|||
{ |
|||
return result; |
|||
} |
|||
else |
|||
{ |
|||
if (r.Take() != ',') |
|||
{ |
|||
throw new ExpressionParseException(r, "Expected ','."); |
|||
} |
|||
|
|||
r.SkipWhitespace(); |
|||
} |
|||
} |
|||
|
|||
if (!r.End) |
|||
{ |
|||
r.Take(); |
|||
return result; |
|||
} |
|||
else |
|||
{ |
|||
throw new ExpressionParseException(r, "Expected ']'."); |
|||
} |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,125 @@ |
|||
// 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; |
|||
|
|||
namespace Perspex.Markup.Binding.Parsers |
|||
{ |
|||
internal static class ExpressionParser |
|||
{ |
|||
public static ExpressionNode Parse(Reader r) |
|||
{ |
|||
var nodes = new List<ExpressionNode>(); |
|||
var state = State.Start; |
|||
|
|||
while (!r.End && state != State.End) |
|||
{ |
|||
switch (state) |
|||
{ |
|||
case State.Start: |
|||
state = ParseStart(r, nodes); |
|||
break; |
|||
|
|||
case State.AfterMember: |
|||
state = ParseAfterMember(r, nodes); |
|||
break; |
|||
|
|||
case State.BeforeMember: |
|||
state = ParseBeforeMember(r, nodes); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (state == State.BeforeMember) |
|||
{ |
|||
throw new ExpressionParseException(r, "Unexpected end of expression."); |
|||
} |
|||
|
|||
for (int n = 0; n < nodes.Count - 1; ++n) |
|||
{ |
|||
nodes[n].Next = nodes[n + 1]; |
|||
} |
|||
|
|||
return nodes.FirstOrDefault(); |
|||
} |
|||
|
|||
private static State ParseStart(Reader r, IList<ExpressionNode> nodes) |
|||
{ |
|||
if (ParseNot(r)) |
|||
{ |
|||
nodes.Add(new LogicalNotNode()); |
|||
return State.Start; |
|||
} |
|||
else |
|||
{ |
|||
var identifier = IdentifierParser.Parse(r); |
|||
|
|||
if (identifier != null) |
|||
{ |
|||
nodes.Add(new PropertyAccessorNode(identifier)); |
|||
return State.AfterMember; |
|||
} |
|||
} |
|||
|
|||
return State.End; |
|||
} |
|||
|
|||
private static State ParseAfterMember(Reader r, IList<ExpressionNode> nodes) |
|||
{ |
|||
if (ParseMemberAccessor(r)) |
|||
{ |
|||
return State.BeforeMember; |
|||
} |
|||
else |
|||
{ |
|||
var args = ArgumentListParser.Parse(r, '[', ']'); |
|||
|
|||
if (args != null) |
|||
{ |
|||
if (args.Count == 0) |
|||
{ |
|||
throw new ExpressionParseException(r, "Indexer may not be empty."); |
|||
} |
|||
|
|||
nodes.Add(new IndexerNode(args)); |
|||
return State.AfterMember; |
|||
} |
|||
} |
|||
|
|||
return State.End; |
|||
} |
|||
|
|||
private static State ParseBeforeMember(Reader r, IList<ExpressionNode> nodes) |
|||
{ |
|||
var identifier = IdentifierParser.Parse(r); |
|||
|
|||
if (identifier != null) |
|||
{ |
|||
nodes.Add(new PropertyAccessorNode(identifier)); |
|||
return State.AfterMember; |
|||
} |
|||
|
|||
return State.End; |
|||
} |
|||
|
|||
private static bool ParseNot(Reader r) |
|||
{ |
|||
return !r.End && r.TakeIf('!'); |
|||
} |
|||
|
|||
private static bool ParseMemberAccessor(Reader r) |
|||
{ |
|||
return !r.End && r.TakeIf('.'); |
|||
} |
|||
|
|||
private enum State |
|||
{ |
|||
Start, |
|||
AfterMember, |
|||
BeforeMember, |
|||
End, |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
// 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.Globalization; |
|||
using System.Text; |
|||
|
|||
namespace Perspex.Markup.Binding.Parsers |
|||
{ |
|||
internal static class IdentifierParser |
|||
{ |
|||
public static string Parse(Reader r) |
|||
{ |
|||
if (IsValidIdentifierStart(r.Peek)) |
|||
{ |
|||
var result = new StringBuilder(); |
|||
|
|||
while (!r.End && IsValidIdentifierChar(r.Peek)) |
|||
{ |
|||
result.Append(r.Take()); |
|||
} |
|||
|
|||
return result.ToString(); |
|||
} |
|||
else |
|||
{ |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
private static bool IsValidIdentifierStart(char c) |
|||
{ |
|||
return char.IsLetter(c) || c == '_'; |
|||
} |
|||
|
|||
private static bool IsValidIdentifierChar(char c) |
|||
{ |
|||
if (IsValidIdentifierStart(c)) |
|||
{ |
|||
return true; |
|||
} |
|||
else |
|||
{ |
|||
var cat = CharUnicodeInfo.GetUnicodeCategory(c); |
|||
return cat == UnicodeCategory.NonSpacingMark || |
|||
cat == UnicodeCategory.SpacingCombiningMark || |
|||
cat == UnicodeCategory.ConnectorPunctuation || |
|||
cat == UnicodeCategory.Format; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
// 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.Text; |
|||
|
|||
namespace Perspex.Markup.Binding.Parsers |
|||
{ |
|||
internal static class LiteralParser |
|||
{ |
|||
public static object Parse(Reader r) |
|||
{ |
|||
if (char.IsDigit(r.Peek)) |
|||
{ |
|||
StringBuilder result = new StringBuilder(); |
|||
|
|||
while (!r.End) |
|||
{ |
|||
if (char.IsDigit(r.Peek)) |
|||
{ |
|||
result.Append(r.Take()); |
|||
} |
|||
else |
|||
{ |
|||
break; |
|||
} |
|||
} |
|||
|
|||
return int.Parse(result.ToString()); |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,44 @@ |
|||
// 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.Parsers |
|||
{ |
|||
internal class Reader |
|||
{ |
|||
private string _s; |
|||
private int _i; |
|||
|
|||
public Reader(string s) |
|||
{ |
|||
_s = s; |
|||
} |
|||
|
|||
public bool End => _i == _s.Length; |
|||
public char Peek => _s[_i]; |
|||
public int Position => _i; |
|||
public char Take() => _s[_i++]; |
|||
|
|||
public void SkipWhitespace() |
|||
{ |
|||
while (!End && char.IsWhiteSpace(Peek)) |
|||
{ |
|||
Take(); |
|||
} |
|||
} |
|||
|
|||
public bool TakeIf(char c) |
|||
{ |
|||
if (Peek == c) |
|||
{ |
|||
Take(); |
|||
return true; |
|||
} |
|||
else |
|||
{ |
|||
return false; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,164 @@ |
|||
// 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.Reactive.Linq; |
|||
using System.Reflection; |
|||
using System.Threading; |
|||
using System.Threading.Tasks; |
|||
using System.Windows.Input; |
|||
|
|||
namespace Perspex.Markup.Binding |
|||
{ |
|||
internal class PropertyAccessorNode : ExpressionNode |
|||
{ |
|||
private PropertyInfo _propertyInfo; |
|||
private IDisposable _subscription; |
|||
|
|||
public PropertyAccessorNode(string propertyName) |
|||
{ |
|||
PropertyName = propertyName; |
|||
} |
|||
|
|||
public string PropertyName { get; } |
|||
|
|||
public override bool SetValue(object value) |
|||
{ |
|||
if (Next != null) |
|||
{ |
|||
return Next.SetValue(value); |
|||
} |
|||
else |
|||
{ |
|||
if (_propertyInfo != null && _propertyInfo.CanWrite) |
|||
{ |
|||
_propertyInfo.SetValue(Target, value); |
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
} |
|||
|
|||
protected override void SubscribeAndUpdate(object target) |
|||
{ |
|||
bool set = false; |
|||
|
|||
if (target != null) |
|||
{ |
|||
_propertyInfo = FindProperty(target, PropertyName); |
|||
|
|||
if (_propertyInfo != null) |
|||
{ |
|||
ReadValue(target); |
|||
set = true; |
|||
|
|||
var inpc = target as INotifyPropertyChanged; |
|||
|
|||
if (inpc != null) |
|||
{ |
|||
inpc.PropertyChanged += PropertyChanged; |
|||
} |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
_propertyInfo = null; |
|||
} |
|||
|
|||
if (!set) |
|||
{ |
|||
CurrentValue = ExpressionValue.None; |
|||
} |
|||
} |
|||
|
|||
protected override void Unsubscribe(object target) |
|||
{ |
|||
var inpc = target as INotifyPropertyChanged; |
|||
|
|||
if (inpc != null) |
|||
{ |
|||
inpc.PropertyChanged -= PropertyChanged; |
|||
} |
|||
} |
|||
|
|||
private static PropertyInfo FindProperty(object target, string propertyName) |
|||
{ |
|||
var typeInfo = target.GetType().GetTypeInfo(); |
|||
|
|||
do |
|||
{ |
|||
var result = typeInfo.GetDeclaredProperty(propertyName); |
|||
|
|||
if (result != null) |
|||
{ |
|||
return result; |
|||
} |
|||
else |
|||
{ |
|||
typeInfo = typeInfo.BaseType?.GetTypeInfo(); |
|||
} |
|||
} while (typeInfo != null); |
|||
|
|||
return null; |
|||
} |
|||
|
|||
private void ReadValue(object target) |
|||
{ |
|||
var value = _propertyInfo.GetValue(target); |
|||
var observable = value as IObservable<object>; |
|||
var command = value as ICommand; |
|||
var task = value as Task; |
|||
bool set = false; |
|||
|
|||
// ReactiveCommand is an IObservable but we want to bind to it, not its value.
|
|||
if (observable != null && command == null) |
|||
{ |
|||
CurrentValue = ExpressionValue.None; |
|||
set = true; |
|||
_subscription = observable |
|||
.ObserveOn(SynchronizationContext.Current) |
|||
.Subscribe(x => CurrentValue = new ExpressionValue(x)); |
|||
} |
|||
else if (task != null) |
|||
{ |
|||
var resultProperty = task.GetType().GetTypeInfo().GetDeclaredProperty("Result"); |
|||
|
|||
if (resultProperty != null) |
|||
{ |
|||
if (task.Status == TaskStatus.RanToCompletion) |
|||
{ |
|||
CurrentValue = new ExpressionValue(resultProperty.GetValue(task)); |
|||
set = true; |
|||
} |
|||
else |
|||
{ |
|||
task.ContinueWith( |
|||
x => CurrentValue = new ExpressionValue(resultProperty.GetValue(task)), |
|||
TaskScheduler.FromCurrentSynchronizationContext()) |
|||
.ConfigureAwait(false); |
|||
} |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
CurrentValue = new ExpressionValue(value); |
|||
set = true; |
|||
} |
|||
|
|||
if (!set) |
|||
{ |
|||
CurrentValue = ExpressionValue.None; |
|||
} |
|||
} |
|||
|
|||
private void PropertyChanged(object sender, PropertyChangedEventArgs e) |
|||
{ |
|||
if (e.PropertyName == PropertyName) |
|||
{ |
|||
ReadValue(sender); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,88 @@ |
|||
<?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\ExpressionParseException.cs" /> |
|||
<Compile Include="Binding\ExpressionSubject.cs" /> |
|||
<Compile Include="Binding\ExpressionValue.cs" /> |
|||
<Compile Include="Binding\LogicalNotNode.cs" /> |
|||
<Compile Include="Binding\IndexerNode.cs" /> |
|||
<Compile Include="Binding\Parsers\ArgumentListParser.cs" /> |
|||
<Compile Include="Binding\Parsers\LiteralParser.cs" /> |
|||
<Compile Include="Binding\Parsers\IdentifierParser.cs" /> |
|||
<Compile Include="Binding\Parsers\ExpressionParser.cs" /> |
|||
<Compile Include="Binding\Parsers\Reader.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="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> |
|||
</ItemGroup> |
|||
<ItemGroup> |
|||
<None Include="packages.config" /> |
|||
</ItemGroup> |
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\Perspex.Base\Perspex.Base.csproj"> |
|||
<Project>{b09b78d8-9b26-48b0-9149-d64a2f120f3f}</Project> |
|||
<Name>Perspex.Base</Name> |
|||
</ProjectReference> |
|||
</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,32 @@ |
|||
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")] |
|||
|
|||
[assembly: InternalsVisibleTo("Perspex.Markup.UnitTests")] |
|||
@ -0,0 +1,8 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<packages> |
|||
<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" /> |
|||
</packages> |
|||
@ -0,0 +1,148 @@ |
|||
// 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.Collections.Generic; |
|||
using System.Linq; |
|||
using Perspex.Markup.Binding; |
|||
using Xunit; |
|||
|
|||
namespace Perspex.Markup.UnitTests.Binding |
|||
{ |
|||
public class ExpressionNodeBuilderTests |
|||
{ |
|||
[Fact] |
|||
public void Should_Build_Single_Property() |
|||
{ |
|||
var result = ToList(ExpressionNodeBuilder.Build("Foo")); |
|||
|
|||
AssertIsProperty(result[0], "Foo"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Build_Underscored_Property() |
|||
{ |
|||
var result = ToList(ExpressionNodeBuilder.Build("_Foo")); |
|||
|
|||
AssertIsProperty(result[0], "_Foo"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Build_Property_Chain() |
|||
{ |
|||
var result = ToList(ExpressionNodeBuilder.Build("Foo.Bar.Baz")); |
|||
|
|||
Assert.Equal(3, result.Count); |
|||
AssertIsProperty(result[0], "Foo"); |
|||
AssertIsProperty(result[1], "Bar"); |
|||
AssertIsProperty(result[2], "Baz"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Build_Negated_Property_Chain() |
|||
{ |
|||
var result = ToList(ExpressionNodeBuilder.Build("!Foo.Bar.Baz")); |
|||
|
|||
Assert.Equal(4, result.Count); |
|||
Assert.IsType<LogicalNotNode>(result[0]); |
|||
AssertIsProperty(result[1], "Foo"); |
|||
AssertIsProperty(result[2], "Bar"); |
|||
AssertIsProperty(result[3], "Baz"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Build_Double_Negated_Property_Chain() |
|||
{ |
|||
var result = ToList(ExpressionNodeBuilder.Build("!!Foo.Bar.Baz")); |
|||
|
|||
Assert.Equal(5, result.Count); |
|||
Assert.IsType<LogicalNotNode>(result[0]); |
|||
Assert.IsType<LogicalNotNode>(result[1]); |
|||
AssertIsProperty(result[2], "Foo"); |
|||
AssertIsProperty(result[3], "Bar"); |
|||
AssertIsProperty(result[4], "Baz"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Build_Indexed_Property() |
|||
{ |
|||
var result = ToList(ExpressionNodeBuilder.Build("Foo[15]")); |
|||
|
|||
Assert.Equal(2, result.Count); |
|||
AssertIsProperty(result[0], "Foo"); |
|||
AssertIsIndexer(result[1], 15); |
|||
Assert.IsType<IndexerNode>(result[1]); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Build_Multiple_Indexed_Property() |
|||
{ |
|||
var result = ToList(ExpressionNodeBuilder.Build("Foo[15,6]")); |
|||
|
|||
Assert.Equal(2, result.Count); |
|||
AssertIsProperty(result[0], "Foo"); |
|||
AssertIsIndexer(result[1], 15, 6); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Build_Multiple_Indexed_Property_With_Space() |
|||
{ |
|||
var result = ToList(ExpressionNodeBuilder.Build("Foo[5, 16]")); |
|||
|
|||
Assert.Equal(2, result.Count); |
|||
AssertIsProperty(result[0], "Foo"); |
|||
AssertIsIndexer(result[1], 5, 16); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Build_Consecutive_Indexers() |
|||
{ |
|||
var result = ToList(ExpressionNodeBuilder.Build("Foo[15][16]")); |
|||
|
|||
Assert.Equal(3, result.Count); |
|||
AssertIsProperty(result[0], "Foo"); |
|||
AssertIsIndexer(result[1], 15); |
|||
AssertIsIndexer(result[2], 16); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Build_Indexed_Property_In_Chain() |
|||
{ |
|||
var result = ToList(ExpressionNodeBuilder.Build("Foo.Bar[5, 6].Baz")); |
|||
|
|||
Assert.Equal(4, result.Count); |
|||
AssertIsProperty(result[0], "Foo"); |
|||
AssertIsProperty(result[1], "Bar"); |
|||
AssertIsIndexer(result[2], 5, 6); |
|||
AssertIsProperty(result[3], "Baz"); |
|||
} |
|||
|
|||
private void AssertIsProperty(ExpressionNode node, string name) |
|||
{ |
|||
Assert.IsType<PropertyAccessorNode>(node); |
|||
|
|||
var p = (PropertyAccessorNode)node; |
|||
Assert.Equal(name, p.PropertyName); |
|||
} |
|||
|
|||
private void AssertIsIndexer(ExpressionNode node, params object[] args) |
|||
{ |
|||
Assert.IsType<IndexerNode>(node); |
|||
|
|||
var e = (IndexerNode)node; |
|||
Assert.Equal(e.Arguments.ToArray(), args.ToArray()); |
|||
} |
|||
|
|||
private List<ExpressionNode> ToList(ExpressionNode node) |
|||
{ |
|||
var result = new List<ExpressionNode>(); |
|||
|
|||
while (node != null) |
|||
{ |
|||
result.Add(node); |
|||
node = node.Next; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,74 @@ |
|||
// 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_Errors |
|||
{ |
|||
[Fact] |
|||
public void Identifier_Cannot_Start_With_Digit() |
|||
{ |
|||
Assert.Throws<ExpressionParseException>( |
|||
() => ExpressionNodeBuilder.Build("1Foo")); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Identifier_Cannot_Start_With_Symbol() |
|||
{ |
|||
Assert.Throws<ExpressionParseException>( |
|||
() => ExpressionNodeBuilder.Build("Foo.%Bar")); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Expression_Cannot_End_With_Period() |
|||
{ |
|||
Assert.Throws<ExpressionParseException>( |
|||
() => ExpressionNodeBuilder.Build("Foo.Bar.")); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Expression_Cannot_Have_Empty_Indexer() |
|||
{ |
|||
Assert.Throws<ExpressionParseException>( |
|||
() => ExpressionNodeBuilder.Build("Foo.Bar[]")); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Expression_Cannot_Have_Extra_Comma_At_Start_Of_Indexer() |
|||
{ |
|||
Assert.Throws<ExpressionParseException>( |
|||
() => ExpressionNodeBuilder.Build("Foo.Bar[,3,4]")); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Expression_Cannot_Have_Extra_Comma_In_Indexer() |
|||
{ |
|||
Assert.Throws<ExpressionParseException>( |
|||
() => ExpressionNodeBuilder.Build("Foo.Bar[3,,4]")); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Expression_Cannot_Have_Extra_Comma_At_End_Of_Indexer() |
|||
{ |
|||
Assert.Throws<ExpressionParseException>( |
|||
() => ExpressionNodeBuilder.Build("Foo.Bar[3,4,]")); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Expression_Cannot_Have_Digit_After_Indexer() |
|||
{ |
|||
Assert.Throws<ExpressionParseException>( |
|||
() => ExpressionNodeBuilder.Build("Foo.Bar[3,4]5")); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Expression_Cannot_Have_Letter_After_Indexer() |
|||
{ |
|||
Assert.Throws<ExpressionParseException>( |
|||
() => ExpressionNodeBuilder.Build("Foo.Bar[3,4]A")); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,113 @@ |
|||
// 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.Collections.ObjectModel; |
|||
using System.Reactive.Linq; |
|||
using Perspex.Markup.Binding; |
|||
using Xunit; |
|||
|
|||
namespace Perspex.Markup.UnitTests.Binding |
|||
{ |
|||
public class ExpressionObserverTests_Indexer |
|||
{ |
|||
[Fact] |
|||
public async void Should_Get_Array_Value() |
|||
{ |
|||
var data = new { Foo = new [] { "foo", "bar" } }; |
|||
var target = new ExpressionObserver(data, "Foo[1]"); |
|||
var result = await target.Take(1); |
|||
|
|||
Assert.True(result.HasValue); |
|||
Assert.Equal("bar", result.Value); |
|||
} |
|||
|
|||
[Fact] |
|||
public async void Should_Get_MultiDimensional_Array_Value() |
|||
{ |
|||
var data = new { Foo = new[,] { { "foo", "bar" }, { "baz", "qux" } } }; |
|||
var target = new ExpressionObserver(data, "Foo[1, 1]"); |
|||
var result = await target.Take(1); |
|||
|
|||
Assert.True(result.HasValue); |
|||
Assert.Equal("qux", result.Value); |
|||
} |
|||
|
|||
[Fact] |
|||
public async void Should_Get_List_Value() |
|||
{ |
|||
var data = new { Foo = new List<string> { "foo", "bar" } }; |
|||
var target = new ExpressionObserver(data, "Foo[1]"); |
|||
var result = await target.Take(1); |
|||
|
|||
Assert.True(result.HasValue); |
|||
Assert.Equal("bar", result.Value); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Track_INCC_Add() |
|||
{ |
|||
var data = new { Foo = new ObservableCollection<string> { "foo", "bar" } }; |
|||
var target = new ExpressionObserver(data, "Foo[2]"); |
|||
var result = new List<object>(); |
|||
|
|||
var sub = target.Subscribe(x => result.Add(x.Value)); |
|||
data.Foo.Add("baz"); |
|||
|
|||
Assert.Equal(new[] { null, "baz" }, result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Track_INCC_Remove() |
|||
{ |
|||
var data = new { Foo = new ObservableCollection<string> { "foo", "bar" } }; |
|||
var target = new ExpressionObserver(data, "Foo[0]"); |
|||
var result = new List<object>(); |
|||
|
|||
var sub = target.Subscribe(x => result.Add(x.Value)); |
|||
data.Foo.RemoveAt(0); |
|||
|
|||
Assert.Equal(new[] { "foo", "bar" }, result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Track_INCC_Replace() |
|||
{ |
|||
var data = new { Foo = new ObservableCollection<string> { "foo", "bar" } }; |
|||
var target = new ExpressionObserver(data, "Foo[1]"); |
|||
var result = new List<object>(); |
|||
|
|||
var sub = target.Subscribe(x => result.Add(x.Value)); |
|||
data.Foo[1] = "baz"; |
|||
|
|||
Assert.Equal(new[] { "bar", "baz" }, result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Track_INCC_Move() |
|||
{ |
|||
var data = new { Foo = new ObservableCollection<string> { "foo", "bar" } }; |
|||
var target = new ExpressionObserver(data, "Foo[1]"); |
|||
var result = new List<object>(); |
|||
|
|||
var sub = target.Subscribe(x => result.Add(x.Value)); |
|||
data.Foo.Move(0, 1); |
|||
|
|||
Assert.Equal(new[] { "bar", "foo" }, result); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Track_INCC_Reset() |
|||
{ |
|||
var data = new { Foo = new ObservableCollection<string> { "foo", "bar" } }; |
|||
var target = new ExpressionObserver(data, "Foo[1]"); |
|||
var result = new List<object>(); |
|||
|
|||
var sub = target.Subscribe(x => result.Add(x.Value)); |
|||
data.Foo.Clear(); |
|||
|
|||
Assert.Equal(new[] { "bar", null }, result); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,97 @@ |
|||
// 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.Linq; |
|||
using Perspex.Markup.Binding; |
|||
using Xunit; |
|||
|
|||
namespace Perspex.Markup.UnitTests.Binding |
|||
{ |
|||
public class ExpressionObserverTests_Negation |
|||
{ |
|||
[Fact] |
|||
public async void Should_Negate_Boolean_Value() |
|||
{ |
|||
var data = new { Foo = true }; |
|||
var target = new ExpressionObserver(data, "!Foo"); |
|||
var result = await target.Take(1); |
|||
|
|||
Assert.True(result.HasValue); |
|||
Assert.Equal(false, result.Value); |
|||
} |
|||
|
|||
[Fact] |
|||
public async void Should_Negate_0() |
|||
{ |
|||
var data = new { Foo = 0 }; |
|||
var target = new ExpressionObserver(data, "!Foo"); |
|||
var result = await target.Take(1); |
|||
|
|||
Assert.True(result.HasValue); |
|||
Assert.Equal(true, result.Value); |
|||
} |
|||
|
|||
[Fact] |
|||
public async void Should_Negate_1() |
|||
{ |
|||
var data = new { Foo = 1 }; |
|||
var target = new ExpressionObserver(data, "!Foo"); |
|||
var result = await target.Take(1); |
|||
|
|||
Assert.True(result.HasValue); |
|||
Assert.Equal(false, result.Value); |
|||
} |
|||
|
|||
[Fact] |
|||
public async void Should_Negate_False_String() |
|||
{ |
|||
var data = new { Foo = "false" }; |
|||
var target = new ExpressionObserver(data, "!Foo"); |
|||
var result = await target.Take(1); |
|||
|
|||
Assert.True(result.HasValue); |
|||
Assert.Equal(true, result.Value); |
|||
} |
|||
|
|||
[Fact] |
|||
public async void Should_Negate_True_String() |
|||
{ |
|||
var data = new { Foo = "True" }; |
|||
var target = new ExpressionObserver(data, "!Foo"); |
|||
var result = await target.Take(1); |
|||
|
|||
Assert.True(result.HasValue); |
|||
Assert.Equal(false, result.Value); |
|||
} |
|||
|
|||
[Fact] |
|||
public async void Should_Return_Empty_For_String_Not_Convertible_To_Boolean() |
|||
{ |
|||
var data = new { Foo = "foo" }; |
|||
var target = new ExpressionObserver(data, "!Foo"); |
|||
var result = await target.Take(1); |
|||
|
|||
Assert.False(result.HasValue); |
|||
} |
|||
|
|||
[Fact] |
|||
public async void Should_Return_Empty_For_Value_Not_Convertible_To_Boolean() |
|||
{ |
|||
var data = new { Foo = new object() }; |
|||
var target = new ExpressionObserver(data, "!Foo"); |
|||
var result = await target.Take(1); |
|||
|
|||
Assert.False(result.HasValue); |
|||
} |
|||
|
|||
[Fact] |
|||
public void SetValue_Should_Throw() |
|||
{ |
|||
var data = new { Foo = "foo" }; |
|||
var target = new ExpressionObserver(data, "!Foo"); |
|||
|
|||
Assert.Throws<NotSupportedException>(() => target.SetValue("bar")); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,68 @@ |
|||
// 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 System.Reactive.Subjects; |
|||
using Perspex.Markup.Binding; |
|||
using Xunit; |
|||
|
|||
namespace Perspex.Markup.UnitTests.Binding |
|||
{ |
|||
public class ExpressionObserverTests_Observable |
|||
{ |
|||
[Fact] |
|||
public void Should_Get_Simple_Observable_Value() |
|||
{ |
|||
using (var sync = UnitTestSynchronizationContext.Begin()) |
|||
{ |
|||
var source = new BehaviorSubject<string>("foo"); |
|||
var data = new { Foo = source }; |
|||
var target = new ExpressionObserver(data, "Foo"); |
|||
var result = new List<object>(); |
|||
|
|||
var sub = target.Subscribe(x => result.Add(x.Value)); |
|||
source.OnNext("bar"); |
|||
sync.ExecutePostedCallbacks(); |
|||
|
|||
Assert.Equal(new[] { null, "foo", "bar" }, result); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Get_Property_Value_From_Observable() |
|||
{ |
|||
using (var sync = UnitTestSynchronizationContext.Begin()) |
|||
{ |
|||
var data = new Class1(); |
|||
var target = new ExpressionObserver(data, "Next.Foo"); |
|||
var result = new List<object>(); |
|||
|
|||
var sub = target.Subscribe(x => result.Add(x.Value)); |
|||
data.Next.OnNext(new Class2("foo")); |
|||
sync.ExecutePostedCallbacks(); |
|||
|
|||
Assert.Equal(new[] { null, "foo" }, result); |
|||
|
|||
sub.Dispose(); |
|||
Assert.Equal(0, data.SubscriptionCount); |
|||
} |
|||
} |
|||
|
|||
private class Class1 : NotifyingBase |
|||
{ |
|||
public Subject<Class2> Next { get; } = new Subject<Class2>(); |
|||
} |
|||
|
|||
private class Class2 : NotifyingBase |
|||
{ |
|||
public Class2(string foo) |
|||
{ |
|||
Foo = foo; |
|||
} |
|||
|
|||
public string Foo { get; } |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,284 @@ |
|||
// 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_Property |
|||
{ |
|||
[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_From_Base_Class() |
|||
{ |
|||
var data = new Class3 { 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 { Next = new Class2 { Bar = "bar" } }; |
|||
var target = new ExpressionObserver(data, "Next.Bar"); |
|||
var result = new List<object>(); |
|||
|
|||
var sub = target.Subscribe(x => result.Add(x.Value)); |
|||
((Class2)data.Next).Bar = "baz"; |
|||
|
|||
Assert.Equal(new[] { "bar", "baz" }, result); |
|||
|
|||
sub.Dispose(); |
|||
|
|||
Assert.Equal(0, data.SubscriptionCount); |
|||
Assert.Equal(0, data.Next.SubscriptionCount); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Track_Property_Chain_Changing() |
|||
{ |
|||
var data = new Class1 { Next = new Class2 { Bar = "bar" } }; |
|||
var target = new ExpressionObserver(data, "Next.Bar"); |
|||
var result = new List<object>(); |
|||
|
|||
var sub = target.Subscribe(x => result.Add(x.Value)); |
|||
var old = data.Next; |
|||
data.Next = new Class2 { Bar = "baz" }; |
|||
|
|||
Assert.Equal(new[] { "bar", "baz" }, result); |
|||
|
|||
sub.Dispose(); |
|||
|
|||
Assert.Equal(0, data.SubscriptionCount); |
|||
Assert.Equal(0, data.Next.SubscriptionCount); |
|||
Assert.Equal(0, old.SubscriptionCount); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Track_Property_Chain_Breaking_With_Null_Then_Mending() |
|||
{ |
|||
var data = new Class1 { Next = new Class2 { Bar = "bar" } }; |
|||
var target = new ExpressionObserver(data, "Next.Bar"); |
|||
var result = new List<object>(); |
|||
|
|||
var sub = target.Subscribe(x => result.Add(x.Value)); |
|||
var old = data.Next; |
|||
data.Next = null; |
|||
data.Next = new Class2 { Bar = "baz" }; |
|||
|
|||
Assert.Equal(new[] { "bar", null, "baz" }, result); |
|||
|
|||
sub.Dispose(); |
|||
|
|||
Assert.Equal(0, data.SubscriptionCount); |
|||
Assert.Equal(0, data.Next.SubscriptionCount); |
|||
Assert.Equal(0, old.SubscriptionCount); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Track_Property_Chain_Breaking_With_Object_Then_Mending() |
|||
{ |
|||
var data = new Class1 { Next = new Class2 { Bar = "bar" } }; |
|||
var target = new ExpressionObserver(data, "Next.Bar"); |
|||
var result = new List<object>(); |
|||
|
|||
var sub = target.Subscribe(x => result.Add(x.Value)); |
|||
var old = data.Next; |
|||
var breaking = new WithoutBar(); |
|||
data.Next = breaking; |
|||
data.Next = new Class2 { Bar = "baz" }; |
|||
|
|||
Assert.Equal(new[] { "bar", null, "baz" }, result); |
|||
|
|||
sub.Dispose(); |
|||
|
|||
Assert.Equal(0, data.SubscriptionCount); |
|||
Assert.Equal(0, data.Next.SubscriptionCount); |
|||
Assert.Equal(0, breaking.SubscriptionCount); |
|||
Assert.Equal(0, old.SubscriptionCount); |
|||
} |
|||
|
|||
[Fact] |
|||
public void SetValue_Should_Set_Simple_Property_Value() |
|||
{ |
|||
var data = new Class1 { Foo = "foo" }; |
|||
var target = new ExpressionObserver(data, "Foo"); |
|||
|
|||
Assert.True(target.SetValue("bar")); |
|||
Assert.Equal("bar", data.Foo); |
|||
} |
|||
|
|||
[Fact] |
|||
public void SetValue_Should_Set_Property_At_The_End_Of_Chain() |
|||
{ |
|||
var data = new Class1 { Next = new Class2 { Bar = "bar" } }; |
|||
var target = new ExpressionObserver(data, "Next.Bar"); |
|||
|
|||
Assert.True(target.SetValue("baz")); |
|||
Assert.Equal("baz", ((Class2)data.Next).Bar); |
|||
} |
|||
|
|||
[Fact] |
|||
public void SetValue_Should_Return_False_For_Missing_Property() |
|||
{ |
|||
var data = new Class1 { Next = new WithoutBar()}; |
|||
var target = new ExpressionObserver(data, "Next.Bar"); |
|||
|
|||
Assert.False(target.SetValue("baz")); |
|||
} |
|||
|
|||
[Fact] |
|||
public void SetValue_Should_Return_False_For_Missing_Object() |
|||
{ |
|||
var data = new Class1(); |
|||
var target = new ExpressionObserver(data, "Next.Bar"); |
|||
|
|||
Assert.False(target.SetValue("baz")); |
|||
} |
|||
|
|||
[Fact] |
|||
public void SetValue_Should_Throw_For_Wrong_Type() |
|||
{ |
|||
var data = new Class1 { Foo = "foo" }; |
|||
var target = new ExpressionObserver(data, "Foo"); |
|||
|
|||
Assert.Throws<ArgumentException>(() => target.SetValue(1.2)); |
|||
} |
|||
|
|||
[Fact] |
|||
public async void Should_Handle_Null_Root() |
|||
{ |
|||
var target = new ExpressionObserver(null, "Foo"); |
|||
var result = await target.Take(1); |
|||
|
|||
Assert.False(result.HasValue); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Can_Replace_Root() |
|||
{ |
|||
var first = new Class1 { Foo = "foo" }; |
|||
var second = new Class1 { Foo = "bar" }; |
|||
var target = new ExpressionObserver(first, "Foo"); |
|||
var result = new List<object>(); |
|||
|
|||
var sub = target.Subscribe(x => result.Add(x.Value)); |
|||
target.Root = second; |
|||
target.Root = null; |
|||
|
|||
Assert.Equal(new[] { "foo", "bar", null }, result); |
|||
|
|||
Assert.Equal(0, first.SubscriptionCount); |
|||
Assert.Equal(0, second.SubscriptionCount); |
|||
} |
|||
|
|||
private interface INext |
|||
{ |
|||
int SubscriptionCount { get; } |
|||
} |
|||
|
|||
private class Class1 : NotifyingBase |
|||
{ |
|||
private string _foo; |
|||
private INext _next; |
|||
|
|||
public string Foo |
|||
{ |
|||
get { return _foo; } |
|||
set |
|||
{ |
|||
_foo = value; |
|||
RaisePropertyChanged(nameof(Foo)); |
|||
} |
|||
} |
|||
|
|||
public INext Next |
|||
{ |
|||
get { return _next; } |
|||
set |
|||
{ |
|||
_next = value; |
|||
RaisePropertyChanged(nameof(Next)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private class Class2 : NotifyingBase, INext |
|||
{ |
|||
private string _bar; |
|||
|
|||
public string Bar |
|||
{ |
|||
get { return _bar; } |
|||
set |
|||
{ |
|||
_bar = value; |
|||
RaisePropertyChanged(nameof(Bar)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private class Class3 : Class1 |
|||
{ |
|||
} |
|||
|
|||
private class WithoutBar : NotifyingBase, INext |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,87 @@ |
|||
// 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 System.Threading; |
|||
using System.Threading.Tasks; |
|||
using Perspex.Markup.Binding; |
|||
using Xunit; |
|||
|
|||
namespace Perspex.Markup.UnitTests.Binding |
|||
{ |
|||
public class ExpressionObserverTests_Task |
|||
{ |
|||
[Fact] |
|||
public void Should_Get_Simple_Task_Value() |
|||
{ |
|||
using (var sync = UnitTestSynchronizationContext.Begin()) |
|||
{ |
|||
var tcs = new TaskCompletionSource<string>(); |
|||
var data = new { Foo = tcs.Task }; |
|||
var target = new ExpressionObserver(data, "Foo"); |
|||
var result = new List<object>(); |
|||
|
|||
var sub = target.Subscribe(x => result.Add(x.Value)); |
|||
tcs.SetResult("foo"); |
|||
sync.ExecutePostedCallbacks(); |
|||
|
|||
Assert.Equal(new object[] { null, "foo" }, result.ToArray()); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Get_Completed_Task_Value() |
|||
{ |
|||
using (var sync = UnitTestSynchronizationContext.Begin()) |
|||
{ |
|||
var data = new { Foo = Task.FromResult("foo") }; |
|||
var target = new ExpressionObserver(data, "Foo"); |
|||
var result = new List<object>(); |
|||
|
|||
var sub = target.Subscribe(x => result.Add(x.Value)); |
|||
|
|||
Assert.Equal(new object[] { "foo" }, result.ToArray()); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Get_Property_Value_From_Task() |
|||
{ |
|||
using (var sync = UnitTestSynchronizationContext.Begin()) |
|||
{ |
|||
var tcs = new TaskCompletionSource<Class2>(); |
|||
var data = new Class1(tcs.Task); |
|||
var target = new ExpressionObserver(data, "Next.Foo"); |
|||
var result = new List<object>(); |
|||
|
|||
var sub = target.Subscribe(x => result.Add(x.Value)); |
|||
tcs.SetResult(new Class2("foo")); |
|||
sync.ExecutePostedCallbacks(); |
|||
|
|||
Assert.Equal(new object[] { null, "foo" }, result.ToArray()); |
|||
} |
|||
} |
|||
|
|||
private class Class1 : NotifyingBase |
|||
{ |
|||
public Class1(Task<Class2> next) |
|||
{ |
|||
Next = next; |
|||
} |
|||
|
|||
public Task<Class2> Next { get; } |
|||
} |
|||
|
|||
private class Class2 : NotifyingBase |
|||
{ |
|||
public Class2(string foo) |
|||
{ |
|||
Foo = foo; |
|||
} |
|||
|
|||
public string Foo { get; } |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,42 @@ |
|||
// 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; |
|||
using System.Linq; |
|||
|
|||
namespace Perspex.Markup.UnitTests.Binding |
|||
{ |
|||
public class NotifyingBase : INotifyPropertyChanged |
|||
{ |
|||
private PropertyChangedEventHandler _propertyChanged; |
|||
|
|||
public event PropertyChangedEventHandler PropertyChanged |
|||
{ |
|||
add |
|||
{ |
|||
_propertyChanged += value; |
|||
++SubscriptionCount; |
|||
} |
|||
|
|||
remove |
|||
{ |
|||
if (_propertyChanged?.GetInvocationList().Contains(value) == true) |
|||
{ |
|||
_propertyChanged -= value; |
|||
--SubscriptionCount; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public int SubscriptionCount |
|||
{ |
|||
get; |
|||
private set; |
|||
} |
|||
|
|||
protected void RaisePropertyChanged(string propertyName) |
|||
{ |
|||
_propertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,117 @@ |
|||
<?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\ExpressionNodeBuilderTests_Errors.cs" /> |
|||
<Compile Include="Binding\ExpressionObserverTests_Observable.cs" /> |
|||
<Compile Include="Binding\ExpressionObserverTests_Task.cs" /> |
|||
<Compile Include="Binding\ExpressionObserverTests_Indexer.cs" /> |
|||
<Compile Include="Binding\ExpressionObserverTests_Negation.cs" /> |
|||
<Compile Include="Binding\ExpressionObserverTests_Property.cs" /> |
|||
<Compile Include="Binding\ExpressionNodeBuilderTests.cs" /> |
|||
<Compile Include="Binding\NotifyingBase.cs" /> |
|||
<Compile Include="Properties\AssemblyInfo.cs" /> |
|||
<Compile Include="UnitTestSynchronizationContext.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> |
|||
<ProjectReference Include="..\..\src\Perspex.Base\Perspex.Base.csproj"> |
|||
<Project>{b09b78d8-9b26-48b0-9149-d64a2f120f3f}</Project> |
|||
<Name>Perspex.Base</Name> |
|||
</ProjectReference> |
|||
</ItemGroup> |
|||
<ItemGroup> |
|||
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" /> |
|||
</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,68 @@ |
|||
// 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.Disposables; |
|||
using System.Threading; |
|||
|
|||
namespace Perspex.Markup.UnitTests |
|||
{ |
|||
internal sealed class UnitTestSynchronizationContext : SynchronizationContext |
|||
{ |
|||
readonly List<Tuple<SendOrPostCallback, object>> _postedCallbacks = |
|||
new List<Tuple<SendOrPostCallback, object>>(); |
|||
|
|||
public static Scope Begin() |
|||
{ |
|||
var sync = new UnitTestSynchronizationContext(); |
|||
var old = SynchronizationContext.Current; |
|||
SynchronizationContext.SetSynchronizationContext(sync); |
|||
return new Scope(old, sync); |
|||
} |
|||
|
|||
public override void Send(SendOrPostCallback d, object state) |
|||
{ |
|||
d(state); |
|||
} |
|||
|
|||
public override void Post(SendOrPostCallback d, object state) |
|||
{ |
|||
lock (_postedCallbacks) |
|||
{ |
|||
_postedCallbacks.Add(Tuple.Create(d, state)); |
|||
} |
|||
} |
|||
|
|||
public void ExecutePostedCallbacks() |
|||
{ |
|||
lock (_postedCallbacks) |
|||
{ |
|||
_postedCallbacks.ForEach(t => t.Item1(t.Item2)); |
|||
_postedCallbacks.Clear(); |
|||
} |
|||
} |
|||
|
|||
public class Scope : IDisposable |
|||
{ |
|||
private SynchronizationContext _old; |
|||
private UnitTestSynchronizationContext _new; |
|||
|
|||
public Scope(SynchronizationContext old, UnitTestSynchronizationContext n) |
|||
{ |
|||
_old = old; |
|||
_new = n; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
SynchronizationContext.SetSynchronizationContext(_old); |
|||
} |
|||
|
|||
public void ExecutePostedCallbacks() |
|||
{ |
|||
_new.ExecutePostedCallbacks(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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> |
|||
@ -1,27 +0,0 @@ |
|||
// 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 Moq; |
|||
using Perspex.Markup.Xaml.DataBinding; |
|||
using OmniXaml.TypeConversion; |
|||
using Xunit; |
|||
|
|||
namespace Perspex.Xaml.Base.UnitTest |
|||
{ |
|||
public class BinderTest |
|||
{ |
|||
[Fact] |
|||
public void NullTarget_Throws() |
|||
{ |
|||
var typeConverter = new Mock<ITypeConverterProvider>(); |
|||
var perspexPropertyBinder = new PerspexPropertyBinder(typeConverter.Object); |
|||
var bindingDefinitionBuilder = new BindingDefinitionBuilder(); |
|||
var binding = bindingDefinitionBuilder |
|||
.WithNullTarget() |
|||
.Build(); |
|||
|
|||
var exception = Assert.Throws<InvalidOperationException>(() => perspexPropertyBinder.Create(binding)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,131 @@ |
|||
// 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.Linq; |
|||
using System.Reactive.Subjects; |
|||
using Moq; |
|||
using Perspex.Controls; |
|||
using Perspex.Markup.Xaml.Binding; |
|||
using Xunit; |
|||
|
|||
namespace Perspex.Markup.Xaml.UnitTests.Binding |
|||
{ |
|||
public class XamlBindingTests |
|||
{ |
|||
[Fact] |
|||
public void OneWay_Binding_Should_Be_Set_Up() |
|||
{ |
|||
var target = CreateTarget(); |
|||
var binding = new XamlBinding |
|||
{ |
|||
SourcePropertyPath = "Foo", |
|||
BindingMode = BindingMode.OneWay, |
|||
}; |
|||
|
|||
binding.Bind(target.Object, TextBox.TextProperty); |
|||
|
|||
target.Verify(x => x.Bind( |
|||
TextBox.TextProperty, |
|||
It.IsAny<IObservable<object>>(), |
|||
BindingPriority.LocalValue)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void TwoWay_Binding_Should_Be_Set_Up() |
|||
{ |
|||
var target = CreateTarget(); |
|||
var binding = new XamlBinding |
|||
{ |
|||
SourcePropertyPath = "Foo", |
|||
BindingMode = BindingMode.TwoWay, |
|||
}; |
|||
|
|||
binding.Bind(target.Object, TextBox.TextProperty); |
|||
|
|||
target.Verify(x => x.BindTwoWay( |
|||
TextBox.TextProperty, |
|||
It.IsAny<ISubject<object>>(), |
|||
BindingPriority.LocalValue)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void OneTime_Binding_Should_Be_Set_Up() |
|||
{ |
|||
var dataContext = new BehaviorSubject<object>(null); |
|||
var expression = new BehaviorSubject<object>(null); |
|||
var target = CreateTarget(dataContext: dataContext); |
|||
var binding = new XamlBinding |
|||
{ |
|||
SourcePropertyPath = "Foo", |
|||
BindingMode = BindingMode.OneTime, |
|||
}; |
|||
|
|||
binding.Bind(target.Object, TextBox.TextProperty, expression); |
|||
|
|||
target.Verify(x => x.SetValue( |
|||
(PerspexProperty)TextBox.TextProperty, |
|||
null, |
|||
BindingPriority.LocalValue)); |
|||
target.ResetCalls(); |
|||
|
|||
expression.OnNext("foo"); |
|||
dataContext.OnNext(1); |
|||
|
|||
target.Verify(x => x.SetValue( |
|||
(PerspexProperty)TextBox.TextProperty, |
|||
"foo", |
|||
BindingPriority.LocalValue)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void OneWayToSource_Binding_Should_Be_Set_Up() |
|||
{ |
|||
var textObservable = new Mock<IObservable<string>>(); |
|||
var expression = new Mock<ISubject<object>>(); |
|||
var target = CreateTarget(text: textObservable.Object); |
|||
var binding = new XamlBinding |
|||
{ |
|||
SourcePropertyPath = "Foo", |
|||
BindingMode = BindingMode.OneWayToSource, |
|||
}; |
|||
|
|||
binding.Bind(target.Object, TextBox.TextProperty, expression.Object); |
|||
|
|||
textObservable.Verify(x => x.Subscribe(expression.Object)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Default_BindingMode_Should_Be_Used() |
|||
{ |
|||
var target = CreateTarget(null); |
|||
var binding = new XamlBinding |
|||
{ |
|||
SourcePropertyPath = "Foo", |
|||
}; |
|||
|
|||
binding.Bind(target.Object, TextBox.TextProperty); |
|||
|
|||
// Default for TextBox.Text is two-way.
|
|||
target.Verify(x => x.BindTwoWay( |
|||
TextBox.TextProperty, |
|||
It.IsAny<ISubject<object>>(), |
|||
BindingPriority.LocalValue)); |
|||
} |
|||
|
|||
private Mock<IObservablePropertyBag> CreateTarget( |
|||
IObservable<object> dataContext = null, |
|||
IObservable<string> text = null) |
|||
{ |
|||
var result = new Mock<IObservablePropertyBag>(); |
|||
|
|||
dataContext = dataContext ?? Observable.Never<object>().StartWith((object)null); |
|||
text = text ?? Observable.Never<string>().StartWith((string)null); |
|||
|
|||
result.Setup(x => x.GetObservable(Control.DataContextProperty)).Returns(dataContext); |
|||
result.Setup(x => x.GetObservable((PerspexProperty)Control.DataContextProperty)).Returns(dataContext); |
|||
result.Setup(x => x.GetObservable((PerspexProperty)TextBox.TextProperty)).Returns(text); |
|||
return result; |
|||
} |
|||
} |
|||
} |
|||
@ -1,39 +0,0 @@ |
|||
// 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 Perspex.Controls; |
|||
using Perspex.Markup.Xaml.DataBinding; |
|||
using Perspex.Markup.Xaml.DataBinding.ChangeTracking; |
|||
|
|||
namespace Perspex.Xaml.Base.UnitTest |
|||
{ |
|||
public class BindingDefinitionBuilder |
|||
{ |
|||
private readonly BindingMode _bindingMode; |
|||
private readonly PropertyPath _sourcePropertyPath; |
|||
private Control _target; |
|||
private PerspexProperty _targetProperty; |
|||
|
|||
public BindingDefinitionBuilder() |
|||
{ |
|||
_bindingMode = BindingMode.Default; |
|||
_sourcePropertyPath = new PropertyPath(string.Empty); |
|||
} |
|||
|
|||
public BindingDefinitionBuilder WithNullTarget() |
|||
{ |
|||
_target = null; |
|||
return this; |
|||
} |
|||
|
|||
public XamlBindingDefinition Build() |
|||
{ |
|||
return new XamlBindingDefinition( |
|||
bindingMode: _bindingMode, |
|||
sourcePropertyPath: _sourcePropertyPath, |
|||
target: _target, |
|||
targetProperty: _targetProperty); |
|||
} |
|||
} |
|||
} |
|||
@ -1,72 +0,0 @@ |
|||
// 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 Perspex.Markup.Xaml.DataBinding.ChangeTracking; |
|||
using Perspex.Xaml.Base.UnitTest.SampleModel; |
|||
using Xunit; |
|||
|
|||
namespace Perspex.Xaml.Base.UnitTest |
|||
{ |
|||
public class ChangeBranchTest |
|||
{ |
|||
[Fact] |
|||
public void GetValueOfMemberOfStruct() |
|||
{ |
|||
var level1 = new Level1(); |
|||
level1.DateTime = new DateTime(1, 2, 3, 4, 5, 6); |
|||
|
|||
var branch = new ObservablePropertyBranch(level1, new PropertyPath("DateTime.Minute")); |
|||
|
|||
var day = branch.Value; |
|||
Assert.Equal(day, branch.Value); |
|||
} |
|||
|
|||
[Fact] |
|||
public void OnePathOnly() |
|||
{ |
|||
var level1 = new Level1(); |
|||
|
|||
var branch = new ObservablePropertyBranch(level1, new PropertyPath("Text")); |
|||
var newValue = "Hey now"; |
|||
branch.Value = newValue; |
|||
|
|||
Assert.Equal(level1.Text, newValue); |
|||
} |
|||
|
|||
[Fact] |
|||
public void SettingValueToUnderlyingProperty_ChangesTheValueInBranch() |
|||
{ |
|||
var level1 = new Level1(); |
|||
|
|||
level1.Level2.Level3.Property = 3; |
|||
|
|||
var branch = new ObservablePropertyBranch(level1, new PropertyPath("Level2.Level3.Property")); |
|||
Assert.Equal(3, branch.Value); |
|||
} |
|||
|
|||
[Fact] |
|||
public void SettingValueToBranch_ChangesTheUnderlyingProperty() |
|||
{ |
|||
var level1 = new Level1(); |
|||
|
|||
var branch = new ObservablePropertyBranch(level1, new PropertyPath("Level2.Level3.Property")); |
|||
branch.Value = 3; |
|||
Assert.Equal(3, level1.Level2.Level3.Property); |
|||
} |
|||
|
|||
[Fact] |
|||
public void SettingValueProperty_RaisesChangeInBranch() |
|||
{ |
|||
var level1 = new Level1(); |
|||
|
|||
var branch = new ObservablePropertyBranch(level1, new PropertyPath("Level2.Level3.Property")); |
|||
bool received = false; |
|||
ObservableExtensions.Subscribe(branch.Values, v => received = ((int)v == 3)); |
|||
|
|||
level1.Level2.Level3.Property = 3; |
|||
|
|||
Assert.True(received); |
|||
} |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue