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.
|
// 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.
|
// 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 |
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