Browse Source

Merge branch 'xaml-binding-datatemplates'

pull/242/merge
Steven Kirk 11 years ago
parent
commit
a4ddaccc15
  1. 2
      .gitmodules
  2. 21
      Perspex.sln
  3. 6
      samples/BindingTest/App.config
  4. 40
      samples/BindingTest/App.cs
  5. 167
      samples/BindingTest/BindingTest.csproj
  6. 22
      samples/BindingTest/MainWindow.paml
  7. 21
      samples/BindingTest/MainWindow.paml.cs
  8. 36
      samples/BindingTest/Properties/AssemblyInfo.cs
  9. 44
      samples/BindingTest/ViewModels/MainWindowViewModel.cs
  10. 15
      samples/BindingTest/ViewModels/TestItem.cs
  11. 10
      samples/BindingTest/packages.config
  12. 2
      samples/TestApplication/GalleryStyle.cs
  13. 4
      samples/TestApplication/Program.cs
  14. 4
      samples/XamlTestApplication/Program.cs
  15. 58
      samples/XamlTestApplicationPcl/ViewModels/MainWindowViewModel.cs
  16. 17
      samples/XamlTestApplicationPcl/ViewModels/TestItem.cs
  17. 14
      samples/XamlTestApplicationPcl/ViewModels/TestNode.cs
  18. 9
      samples/XamlTestApplicationPcl/Views/MainWindow.cs
  19. 47
      samples/XamlTestApplicationPcl/Views/MainWindow.paml
  20. 3
      samples/XamlTestApplicationPcl/XamlTestApplicationPcl.csproj
  21. 2
      src/Markup/Perspex.Markup.Xaml/Binding/SourceBindingEndpoint.cs
  22. 2
      src/Markup/Perspex.Markup.Xaml/Binding/TargetBindingEndpoint.cs
  23. 70
      src/Markup/Perspex.Markup.Xaml/Binding/XamlBinding.cs
  24. 21
      src/Markup/Perspex.Markup.Xaml/Binding/XamlBindingDefinition.cs
  25. 3
      src/Markup/Perspex.Markup.Xaml/Context/PerspexObjectAssembler.cs
  26. 9
      src/Markup/Perspex.Markup.Xaml/Context/PerspexTypeRepository.cs
  27. 8
      src/Markup/Perspex.Markup.Xaml/Context/PerspexWiringContext.cs
  28. 10
      src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMember.cs
  29. 61
      src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs
  30. 12
      src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlType.cs
  31. 109
      src/Markup/Perspex.Markup.Xaml/DataBinding/ChangeTracking/ObservablePropertyBranch.cs
  32. 52
      src/Markup/Perspex.Markup.Xaml/DataBinding/ChangeTracking/PropertyMountPoint.cs
  33. 36
      src/Markup/Perspex.Markup.Xaml/DataBinding/ChangeTracking/PropertyPath.cs
  34. 41
      src/Markup/Perspex.Markup.Xaml/DataBinding/ChangeTracking/TargettedProperty.cs
  35. 165
      src/Markup/Perspex.Markup.Xaml/DataBinding/DataContextChangeSynchronizer.cs
  36. 16
      src/Markup/Perspex.Markup.Xaml/DataBinding/IPerspexPropertyBinder.cs
  37. 59
      src/Markup/Perspex.Markup.Xaml/DataBinding/PerspexPropertyBinder.cs
  38. 65
      src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBinding.cs
  39. 32
      src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBindingDefinition.cs
  40. 18
      src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs
  41. 2
      src/Markup/Perspex.Markup.Xaml/OmniXAML
  42. 22
      src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj
  43. 26
      src/Markup/Perspex.Markup.Xaml/Templates/DataTemplate.cs
  44. 53
      src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs
  45. 102
      src/Markup/Perspex.Markup/Binding/ExpressionNode.cs
  46. 29
      src/Markup/Perspex.Markup/Binding/ExpressionNodeBuilder.cs
  47. 115
      src/Markup/Perspex.Markup/Binding/ExpressionObserver.cs
  48. 24
      src/Markup/Perspex.Markup/Binding/ExpressionParseException.cs
  49. 51
      src/Markup/Perspex.Markup/Binding/ExpressionSubject.cs
  50. 38
      src/Markup/Perspex.Markup/Binding/ExpressionValue.cs
  51. 106
      src/Markup/Perspex.Markup/Binding/IndexerNode.cs
  52. 40
      src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs
  53. 67
      src/Markup/Perspex.Markup/Binding/Parsers/ArgumentListParser.cs
  54. 125
      src/Markup/Perspex.Markup/Binding/Parsers/ExpressionParser.cs
  55. 51
      src/Markup/Perspex.Markup/Binding/Parsers/IdentifierParser.cs
  56. 34
      src/Markup/Perspex.Markup/Binding/Parsers/LiteralParser.cs
  57. 44
      src/Markup/Perspex.Markup/Binding/Parsers/Reader.cs
  58. 164
      src/Markup/Perspex.Markup/Binding/PropertyAccessorNode.cs
  59. 88
      src/Markup/Perspex.Markup/Perspex.Markup.csproj
  60. 32
      src/Markup/Perspex.Markup/Properties/AssemblyInfo.cs
  61. 8
      src/Markup/Perspex.Markup/packages.config
  62. 2
      src/Perspex.Base/BindingDescriptor.cs
  63. 38
      src/Perspex.Base/IObservablePropertyBag.cs
  64. 86
      src/Perspex.Base/PerspexObject.cs
  65. 26
      src/Perspex.Base/PriorityValue.cs
  66. 2
      src/Perspex.Controls/ContentControl.cs
  67. 4
      src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs
  68. 2
      src/Perspex.Controls/IContentControl.cs
  69. 2
      src/Perspex.Controls/ItemsControl.cs
  70. 8
      src/Perspex.Controls/Perspex.Controls.csproj
  71. 2
      src/Perspex.Controls/Primitives/SelectingItemsControl.cs
  72. 1
      src/Perspex.Controls/Properties/AssemblyInfo.cs
  73. 2
      src/Perspex.Controls/Templates/DataTemplateExtensions.cs
  74. 14
      src/Perspex.Controls/Templates/FuncDataTemplate.cs
  75. 10
      src/Perspex.Controls/Templates/FuncDataTemplate`1.cs
  76. 18
      src/Perspex.Controls/Templates/FuncTreeDataTemplate.cs
  77. 18
      src/Perspex.Controls/Templates/FuncTreeDataTemplate`1.cs
  78. 2
      src/Perspex.Controls/TextBlock.cs
  79. 2
      src/Perspex.Diagnostics/Views/LogicalTreeView.cs
  80. 2
      src/Perspex.Diagnostics/Views/VisualTreeView.cs
  81. 4
      src/Perspex.Themes.Default/MenuItemStyle.cs
  82. 9
      tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs
  83. 2
      tests/Perspex.Controls.UnitTests/ItemsControlTests.cs
  84. 2
      tests/Perspex.Controls.UnitTests/TabControlTests.cs
  85. 148
      tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests.cs
  86. 74
      tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests_Errors.cs
  87. 113
      tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Indexer.cs
  88. 97
      tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Negation.cs
  89. 68
      tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Observable.cs
  90. 284
      tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Property.cs
  91. 87
      tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Task.cs
  92. 42
      tests/Perspex.Markup.UnitTests/Binding/NotifyingBase.cs
  93. 117
      tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj
  94. 36
      tests/Perspex.Markup.UnitTests/Properties/AssemblyInfo.cs
  95. 68
      tests/Perspex.Markup.UnitTests/UnitTestSynchronizationContext.cs
  96. 14
      tests/Perspex.Markup.UnitTests/packages.config
  97. 27
      tests/Perspex.Markup.Xaml.UnitTests/BinderTest.cs
  98. 131
      tests/Perspex.Markup.Xaml.UnitTests/Binding/XamlBindingTests.cs
  99. 39
      tests/Perspex.Markup.Xaml.UnitTests/BindingDefinitionBuilder.cs
  100. 72
      tests/Perspex.Markup.Xaml.UnitTests/ChangeBranchTest.cs

2
.gitmodules

@ -7,4 +7,4 @@
branch = perspex-pcl
[submodule "src/Markup/Perspex.Markup.Xaml/OmniXAML"]
path = src/Markup/Perspex.Markup.Xaml/OmniXAML
url = https://github.com/SuperJMN/OmniXAML.git
url = https://github.com/Perspex/OmniXAML.git

21
Perspex.sln

@ -96,6 +96,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.HtmlRenderer", "src
EndProject
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "PlatformSupport", "src\Shared\PlatformSupport\PlatformSupport.shproj", "{E4D9629C-F168-4224-3F51-A5E482FFBC42}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Markup", "src\Markup\Perspex.Markup\Perspex.Markup.csproj", "{6417E941-21BC-467B-A771-0DE389353CE6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Perspex.Markup.UnitTests", "tests\Perspex.Markup.UnitTests\Perspex.Markup.UnitTests.csproj", "{8EF392D5-1416-45AA-9956-7CBBC3229E8A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BindingTest", "samples\BindingTest\BindingTest.csproj", "{08B3E6B9-1CD5-443C-9F61-6D49D1C5F162}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XamlTestApplicationPcl", "samples\XamlTestApplicationPcl\XamlTestApplicationPcl.csproj", "{EA113F1A-D8D7-4142-9948-353270E7EBAE}"
EndProject
Global
@ -237,6 +243,18 @@ Global
{5FB2B005-0A7F-4DAD-ADD4-3ED01444E63D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5FB2B005-0A7F-4DAD-ADD4-3ED01444E63D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5FB2B005-0A7F-4DAD-ADD4-3ED01444E63D}.Release|Any CPU.Build.0 = Release|Any CPU
{6417E941-21BC-467B-A771-0DE389353CE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6417E941-21BC-467B-A771-0DE389353CE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6417E941-21BC-467B-A771-0DE389353CE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6417E941-21BC-467B-A771-0DE389353CE6}.Release|Any CPU.Build.0 = Release|Any CPU
{8EF392D5-1416-45AA-9956-7CBBC3229E8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8EF392D5-1416-45AA-9956-7CBBC3229E8A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8EF392D5-1416-45AA-9956-7CBBC3229E8A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8EF392D5-1416-45AA-9956-7CBBC3229E8A}.Release|Any CPU.Build.0 = Release|Any CPU
{08B3E6B9-1CD5-443C-9F61-6D49D1C5F162}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{08B3E6B9-1CD5-443C-9F61-6D49D1C5F162}.Debug|Any CPU.Build.0 = Debug|Any CPU
{08B3E6B9-1CD5-443C-9F61-6D49D1C5F162}.Release|Any CPU.ActiveCfg = Release|Any CPU
{08B3E6B9-1CD5-443C-9F61-6D49D1C5F162}.Release|Any CPU.Build.0 = Release|Any CPU
{EA113F1A-D8D7-4142-9948-353270E7EBAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EA113F1A-D8D7-4142-9948-353270E7EBAE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EA113F1A-D8D7-4142-9948-353270E7EBAE}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -266,6 +284,9 @@ Global
{54F237D5-A70A-4752-9656-0C70B1A7B047} = {B9894058-278A-46B5-B6ED-AD613FCC03B3}
{FB05AC90-89BA-4F2F-A924-F37875FB547C} = {B9894058-278A-46B5-B6ED-AD613FCC03B3}
{E4D9629C-F168-4224-3F51-A5E482FFBC42} = {A689DEF5-D50F-4975-8B72-124C9EB54066}
{6417E941-21BC-467B-A771-0DE389353CE6} = {8B6A8209-894F-4BA1-B880-965FD453982C}
{8EF392D5-1416-45AA-9956-7CBBC3229E8A} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{08B3E6B9-1CD5-443C-9F61-6D49D1C5F162} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{EA113F1A-D8D7-4142-9948-353270E7EBAE} = {9B9E3891-2366-4253-A952-D08BCEB71098}
EndGlobalSection
EndGlobal

6
samples/BindingTest/App.config

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6" />
</startup>
</configuration>

40
samples/BindingTest/App.cs

@ -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);
}
}
}

167
samples/BindingTest/BindingTest.csproj

@ -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>

22
samples/BindingTest/MainWindow.paml

@ -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>

21
samples/BindingTest/MainWindow.paml.cs

@ -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);
}
}
}

36
samples/BindingTest/Properties/AssemblyInfo.cs

@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("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")]

44
samples/BindingTest/ViewModels/MainWindowViewModel.cs

@ -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); }
}
}
}

15
samples/BindingTest/ViewModels/TestItem.cs

@ -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); }
}
}
}

10
samples/BindingTest/packages.config

@ -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>

2
samples/TestApplication/GalleryStyle.cs

@ -61,7 +61,7 @@ namespace TestApplication
{
DataTemplates = new DataTemplates
{
new DataTemplate<string>(x => new Border
new FuncDataTemplate<string>(x => new Border
{
[~Border.BackgroundProperty] = control[~TemplatedControl.BackgroundProperty],
Padding = new Thickness(10),

4
samples/TestApplication/Program.cs

@ -86,7 +86,7 @@ namespace TestApplication
{
DataTemplates = new DataTemplates
{
new TreeDataTemplate<Node>(
new FuncTreeDataTemplate<Node>(
x => new TextBlock { Text = x.Name },
x => x.Children,
x => true),
@ -405,7 +405,7 @@ namespace TestApplication
Margin = new Thickness(10),
DataTemplates = new DataTemplates
{
new DataTemplate<Item>(x =>
new FuncDataTemplate<Item>(x =>
new StackPanel
{
Gap = 4,

4
samples/XamlTestApplication/Program.cs

@ -23,10 +23,10 @@ namespace XamlTestApplication
{
};
var window = new MainWindow();
window.Show();
Application.Current.Run(window);
}
}
}
}

58
samples/XamlTestApplicationPcl/ViewModels/MainWindowViewModel.cs

@ -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; }
}
}

17
samples/XamlTestApplicationPcl/ViewModels/TestItem.cs

@ -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; }
}
}

14
samples/XamlTestApplicationPcl/ViewModels/TestNode.cs

@ -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; }
}
}

9
samples/XamlTestApplicationPcl/Views/MainWindow.cs

@ -1,15 +1,10 @@
// 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.IO;
using System.Reflection;
using System.Resources;
using OmniXaml;
using Perspex.Controls;
using Perspex.Diagnostics;
using Perspex.Markup.Xaml;
using XamlTestApplication.ViewModels;
namespace XamlTestApplication.Views
{
@ -18,7 +13,7 @@ namespace XamlTestApplication.Views
public MainWindow()
{
InitializeComponent();
DataContext = new MainWindowViewModel();
DevTools.Attach(this);
}

47
samples/XamlTestApplicationPcl/Views/MainWindow.paml

@ -1,6 +1,7 @@
<Window x:Class="XamlTestApplication.MainWindow"
xmlns="https://github.com/perspex"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:XamlTestApplication.ViewModels;assembly=XamlTestApplication"
Title="Perspex Test Application" Height="350" Width="525" SizeToContent="WidthAndHeight" >
<Grid RowDefinitions="Auto,*,Auto" ColumnDefinitions="*,*">
<TabControl Grid.Row="1" Grid.ColumnSpan="2" Padding="5">
@ -48,30 +49,36 @@
</TabItem>
<TabItem Header="Lists">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
<ListBox SelectedIndex="0">
<ListBoxItem>
<StackPanel>
<TextBlock Text="Item 1" FontSize="24" />
<TextBlock Text="Item 1 Value" />
</StackPanel>
</ListBoxItem>
<ListBoxItem>
<StackPanel>
<TextBlock Text="Item 2" FontSize="24" />
<TextBlock Text="Item 2 Value" />
</StackPanel>
</ListBoxItem>
</ListBox>
<DropDown VerticalAlignment="Center" SelectedIndex="0">
<ListBox Items="{Binding Items}">
<ListBox.DataTemplates>
<DataTemplate DataType="vm:TestItem">
<StackPanel>
<TextBlock Text="Item 1" FontSize="24" />
<TextBlock Text="Item 1 Value" />
<TextBlock Text="{Binding Header}" FontSize="24"/>
<TextBlock Text="{Binding SubHeader}"/>
</StackPanel>
</DataTemplate>
</ListBox.DataTemplates>
</ListBox>
<DropDown VerticalAlignment="Center" SelectedIndex="0">
<StackPanel>
<TextBlock Text="Item 1" FontSize="24" />
<TextBlock Text="Item 1 Value" />
</StackPanel>
<StackPanel>
<TextBlock Text="Item 2" FontSize="24" />
<TextBlock Text="Item 2 Value" />
</StackPanel>
</DropDown>
<TreeView Items="{Binding Nodes}">
<TreeView.DataTemplates>
<TreeDataTemplate DataType="vm:TestNode" ItemsSource="{Binding Children}">
<StackPanel>
<TextBlock Text="Item 2" FontSize="24" />
<TextBlock Text="Item 2 Value" />
<TextBlock Text="{Binding Header}" FontSize="24"/>
<TextBlock Text="{Binding SubHeader}"/>
</StackPanel>
</DropDown>
</TreeDataTemplate>
</TreeView.DataTemplates>
</TreeView>
</StackPanel>
</TabItem>
<TabItem Header="Layout">

3
samples/XamlTestApplicationPcl/XamlTestApplicationPcl.csproj

@ -41,6 +41,9 @@
</ItemGroup>
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ViewModels\MainWindowViewModel.cs" />
<Compile Include="ViewModels\TestItem.cs" />
<Compile Include="ViewModels\TestNode.cs" />
<Compile Include="Views\MainWindow.cs" />
<Compile Include="XamlTestApp.cs" />
</ItemGroup>

2
src/Markup/Perspex.Markup.Xaml/DataBinding/SourceBindingEndpoint.cs → src/Markup/Perspex.Markup.Xaml/Binding/SourceBindingEndpoint.cs

@ -4,7 +4,7 @@
using System;
using System.ComponentModel;
namespace Perspex.Markup.Xaml.DataBinding
namespace Perspex.Markup.Xaml.Binding
{
public class SourceBindingEndpoint
{

2
src/Markup/Perspex.Markup.Xaml/DataBinding/TargetBindingEndpoint.cs → src/Markup/Perspex.Markup.Xaml/Binding/TargetBindingEndpoint.cs

@ -1,7 +1,7 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
namespace Perspex.Markup.Xaml.DataBinding
namespace Perspex.Markup.Xaml.Binding
{
public class TargetBindingEndpoint
{

70
src/Markup/Perspex.Markup.Xaml/Binding/XamlBinding.cs

@ -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;
}
}
}
}

21
src/Markup/Perspex.Markup.Xaml/Binding/XamlBindingDefinition.cs

@ -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; }
}
}

3
src/Markup/Perspex.Markup.Xaml/Context/PerspexObjectAssembler.cs

@ -15,7 +15,8 @@ namespace Perspex.Markup.Xaml.Context
public PerspexObjectAssembler(IWiringContext wiringContext, ObjectAssemblerSettings objectAssemblerSettings = null)
{
var mapping = new DeferredLoaderMapping();
mapping.Map<XamlDataTemplate>(template => template.Content, new TemplateLoader());
mapping.Map<DataTemplate>(template => template.Content, new TemplateLoader());
mapping.Map<TreeDataTemplate>(template => template.Content, new TemplateLoader());
var assembler = new ObjectAssembler(wiringContext, new TopDownValueContext(), objectAssemblerSettings);
_objectAssembler = new TemplateHostingObjectAssembler(assembler, mapping);

9
src/Markup/Perspex.Markup.Xaml/Context/PerspexTypeRepository.cs

@ -5,28 +5,25 @@ using System;
using Glass;
using OmniXaml;
using OmniXaml.Typing;
using Perspex.Markup.Xaml.DataBinding;
using Perspex.Markup.Xaml.Binding;
namespace Perspex.Markup.Xaml.Context
{
public class PerspexTypeRepository : XamlTypeRepository
{
private readonly ITypeFactory _typeFactory;
private readonly IPerspexPropertyBinder _propertyBinder;
public PerspexTypeRepository(IXamlNamespaceRegistry xamlNamespaceRegistry,
ITypeFactory typeFactory,
ITypeFeatureProvider featureProvider,
IPerspexPropertyBinder propertyBinder) : base(xamlNamespaceRegistry, typeFactory, featureProvider)
ITypeFeatureProvider featureProvider) : base(xamlNamespaceRegistry, typeFactory, featureProvider)
{
_typeFactory = typeFactory;
_propertyBinder = propertyBinder;
}
public override XamlType GetXamlType(Type type)
{
Guard.ThrowIfNull(type, nameof(type));
return new PerspexXamlType(type, this, _typeFactory, FeatureProvider, _propertyBinder);
return new PerspexXamlType(type, this, _typeFactory, FeatureProvider);
}
}
}

8
src/Markup/Perspex.Markup.Xaml/Context/PerspexWiringContext.cs

@ -15,7 +15,7 @@ using Perspex.Controls;
using Perspex.Input;
using Perspex.Markup.Xaml.Templates;
using Perspex.Markup.Xaml.Converters;
using Perspex.Markup.Xaml.DataBinding;
using Perspex.Markup.Xaml.Binding;
using Perspex.Markup.Xaml.MarkupExtensions;
using Perspex.Media;
using Perspex.Media.Imaging;
@ -43,8 +43,7 @@ namespace Perspex.Markup.Xaml.Context
private static ITypeContext CreateTypeContext(ITypeFactory typeFactory, TypeFeatureProvider featureProvider)
{
var xamlNamespaceRegistry = CreateXamlNamespaceRegistry();
var perspexPropertyBinder = new PerspexPropertyBinder(featureProvider.ConverterProvider);
var typeRepository = new PerspexTypeRepository(xamlNamespaceRegistry, typeFactory, featureProvider, perspexPropertyBinder);
var typeRepository = new PerspexTypeRepository(xamlNamespaceRegistry, typeFactory, featureProvider);
typeRepository.RegisterMetadata(new Metadata<Setter>().WithMemberDependency(x => x.Value, x => x.Property));
typeRepository.RegisterMetadata(
@ -116,6 +115,7 @@ namespace Perspex.Markup.Xaml.Context
var contentProperties = new Collection<ContentPropertyDefinition>
{
new ContentPropertyDefinition(typeof(ContentControl), "Content"),
new ContentPropertyDefinition(typeof(DataTemplate), "Content"),
new ContentPropertyDefinition(typeof(Decorator), "Child"),
new ContentPropertyDefinition(typeof(ItemsControl), "Items"),
new ContentPropertyDefinition(typeof(GradientBrush), "GradientStops"),
@ -123,7 +123,7 @@ namespace Perspex.Markup.Xaml.Context
new ContentPropertyDefinition(typeof(Style), "Setters"),
new ContentPropertyDefinition(typeof(TextBlock), "Text"),
new ContentPropertyDefinition(typeof(TextBox), "Text"),
new ContentPropertyDefinition(typeof(XamlDataTemplate), "Content"),
new ContentPropertyDefinition(typeof(TreeDataTemplate), "Content"),
};
contentPropertyProvider.AddAll(contentProperties);

10
src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMember.cs

@ -1,7 +1,7 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Perspex.Markup.Xaml.DataBinding;
using Perspex.Markup.Xaml.Binding;
using OmniXaml;
using OmniXaml.Typing;
@ -9,21 +9,17 @@ namespace Perspex.Markup.Xaml.Context
{
public class PerspexXamlMember : XamlMember
{
private readonly IPerspexPropertyBinder _propertyBinder;
public PerspexXamlMember(string name,
XamlType owner,
IXamlTypeRepository xamlTypeRepository,
ITypeFeatureProvider featureProvider,
IPerspexPropertyBinder propertyBinder)
ITypeFeatureProvider featureProvider)
: base(name, owner, xamlTypeRepository, featureProvider)
{
_propertyBinder = propertyBinder;
}
protected override IXamlMemberValuePlugin LookupXamlMemberValueConnector()
{
return new PerspexXamlMemberValuePlugin(this, _propertyBinder);
return new PerspexXamlMemberValuePlugin(this);
}
public override string ToString()

61
src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs

@ -2,12 +2,14 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Linq;
using System.Reactive.Linq;
using System.Reflection;
using Glass;
using OmniXaml.ObjectAssembler;
using OmniXaml.Typing;
using Perspex.Controls;
using Perspex.Markup.Xaml.DataBinding;
using Perspex.Markup.Xaml.Binding;
using Perspex.Styling;
namespace Perspex.Markup.Xaml.Context
@ -15,19 +17,18 @@ namespace Perspex.Markup.Xaml.Context
public class PerspexXamlMemberValuePlugin : MemberValuePlugin
{
private readonly XamlMember _xamlMember;
private readonly IPerspexPropertyBinder _propertyBinder;
public PerspexXamlMemberValuePlugin(XamlMember xamlMember, IPerspexPropertyBinder propertyBinder) : base(xamlMember)
public PerspexXamlMemberValuePlugin(XamlMember xamlMember)
: base(xamlMember)
{
_xamlMember = xamlMember;
_propertyBinder = propertyBinder;
}
public override void SetValue(object instance, object value)
{
if (value is XamlBindingDefinition)
{
HandleXamlBindingDefinition((XamlBindingDefinition)value);
HandleXamlBindingDefinition(instance, (XamlBindingDefinition)value);
}
else if (IsPerspexProperty)
{
@ -54,22 +55,48 @@ namespace Perspex.Markup.Xaml.Context
po.SetValue(pp, value);
}
private void HandleXamlBindingDefinition(XamlBindingDefinition xamlBindingDefinition)
private void HandleXamlBindingDefinition(object instance, XamlBindingDefinition def)
{
PerspexObject subjectObject = xamlBindingDefinition.Target;
_propertyBinder.Create(xamlBindingDefinition);
if (_xamlMember.XamlType.UnderlyingType == typeof(XamlBindingDefinition))
{
// TODO: This should search base classes.
var property = instance.GetType().GetTypeInfo().GetDeclaredProperty(_xamlMember.Name);
var observableForDataContext = subjectObject.GetObservable(Control.DataContextProperty);
observableForDataContext.Where(o => o != null).Subscribe(_ => BindToDataContextWhenItsSet(xamlBindingDefinition));
}
if (property == null || !property.CanWrite)
{
throw new InvalidOperationException(
$"Cannot assign to '{_xamlMember.Name}' on '{instance.GetType()}");
}
private void BindToDataContextWhenItsSet(XamlBindingDefinition definition)
{
var target = definition.Target;
var dataContext = target.DataContext;
property.SetValue(instance, def);
}
else
{
var perspexObject = instance as PerspexObject;
var binding = _propertyBinder.GetBinding(target, definition.TargetProperty);
binding.BindToDataContext(dataContext);
if (perspexObject == null)
{
throw new InvalidOperationException(
$"Cannot bind to an object of type '{instance.GetType()}");
}
var property = perspexObject.GetRegisteredProperties()
.FirstOrDefault(x => x.Name == _xamlMember.Name);
if (property == null)
{
throw new InvalidOperationException(
$"Cannot find '{_xamlMember.Name}' on '{instance.GetType()}");
}
var binding = new XamlBinding
{
BindingMode = def.BindingMode,
SourcePropertyPath = def.SourcePropertyPath,
};
binding.Bind(perspexObject, property);
}
}
// ReSharper disable once MemberCanBePrivate.Global

12
src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlType.cs

@ -4,28 +4,22 @@
using System;
using OmniXaml;
using OmniXaml.Typing;
using Perspex.Markup.Xaml.DataBinding;
using Perspex.Markup.Xaml.Binding;
namespace Perspex.Markup.Xaml.Context
{
public class PerspexXamlType : XamlType
{
private readonly IPerspexPropertyBinder _propertyBinder;
public PerspexXamlType(Type type,
IXamlTypeRepository typeRepository,
ITypeFactory typeFactory,
ITypeFeatureProvider featureProvider,
IPerspexPropertyBinder propertyBinder) : base(type, typeRepository, typeFactory, featureProvider)
ITypeFeatureProvider featureProvider) : base(type, typeRepository, typeFactory, featureProvider)
{
_propertyBinder = propertyBinder;
}
protected IPerspexPropertyBinder PropertyBinder => _propertyBinder;
protected override XamlMember LookupMember(string name)
{
return new PerspexXamlMember(name, this, TypeRepository, FeatureProvider, _propertyBinder);
return new PerspexXamlMember(name, this, TypeRepository, FeatureProvider);
}
public override string ToString()

109
src/Markup/Perspex.Markup.Xaml/DataBinding/ChangeTracking/ObservablePropertyBranch.cs

@ -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; }
}
}
}

52
src/Markup/Perspex.Markup.Xaml/DataBinding/ChangeTracking/PropertyMountPoint.cs

@ -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;
}
}

36
src/Markup/Perspex.Markup.Xaml/DataBinding/ChangeTracking/PropertyPath.cs

@ -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);
}
}
}

41
src/Markup/Perspex.Markup.Xaml/DataBinding/ChangeTracking/TargettedProperty.cs

@ -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;
}
}

165
src/Markup/Perspex.Markup.Xaml/DataBinding/DataContextChangeSynchronizer.cs

@ -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;
}
}
}

16
src/Markup/Perspex.Markup.Xaml/DataBinding/IPerspexPropertyBinder.cs

@ -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);
}
}

59
src/Markup/Perspex.Markup.Xaml/DataBinding/PerspexPropertyBinder.cs

@ -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;
}
}
}

65
src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBinding.cs

@ -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);
}
}
}
}

32
src/Markup/Perspex.Markup.Xaml/DataBinding/XamlBindingDefinition.cs

@ -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;
}
}

18
src/Markup/Perspex.Markup.Xaml/MarkupExtensions/BindingExtension.cs

@ -4,8 +4,7 @@
using System.Linq;
using OmniXaml;
using Perspex.Controls;
using Perspex.Markup.Xaml.DataBinding;
using Perspex.Markup.Xaml.DataBinding.ChangeTracking;
using Perspex.Markup.Xaml.Binding;
namespace Perspex.Markup.Xaml.MarkupExtensions
{
@ -22,23 +21,10 @@ namespace Perspex.Markup.Xaml.MarkupExtensions
public override object ProvideValue(MarkupExtensionContext extensionContext)
{
var target = extensionContext.TargetObject as Control;
var targetProperty = extensionContext.TargetProperty;
var targetPropertyName = targetProperty.Name;
var perspexProperty = target.GetRegisteredProperties().First(property => property.Name == targetPropertyName);
return new XamlBindingDefinition
(
target,
perspexProperty,
new PropertyPath(Path),
Mode == BindingMode.Default ? BindingMode.OneWay : Mode
);
return new XamlBindingDefinition(Path, Mode);
}
/// <summary> The source path (for CLR bindings).</summary>
public string Path { get; set; }
public BindingMode Mode { get; set; }
}
}

2
src/Markup/Perspex.Markup.Xaml/OmniXAML

@ -1 +1 @@
Subproject commit 49e6ec001f5873cf2290e0bc1f6f06ca9b9cf808
Subproject commit 42b0e3b6efc0905457120752be38a6000898fffa

22
src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj

@ -38,6 +38,10 @@
<Compile Include="..\..\Shared\SharedAssemblyInfo.cs">
<Link>Properties\SharedAssemblyInfo.cs</Link>
</Compile>
<Compile Include="Binding\SourceBindingEndpoint.cs" />
<Compile Include="Binding\TargetBindingEndpoint.cs" />
<Compile Include="Binding\XamlBinding.cs" />
<Compile Include="Binding\XamlBindingDefinition.cs" />
<Compile Include="Converters\ColorTypeConverter.cs" />
<Compile Include="Converters\KeyGestureConverter.cs" />
<Compile Include="Converters\RelativePointTypeConverter.cs" />
@ -54,10 +58,6 @@
<Compile Include="Converters\BrushTypeConverter.cs" />
<Compile Include="Converters\BitmapTypeConverter.cs" />
<Compile Include="Converters\GridLengthTypeConverter.cs" />
<Compile Include="DataBinding\ChangeTracking\ObservablePropertyBranch.cs" />
<Compile Include="DataBinding\ChangeTracking\PropertyMountPoint.cs" />
<Compile Include="DataBinding\ChangeTracking\PropertyPath.cs" />
<Compile Include="DataBinding\ChangeTracking\TargettedProperty.cs" />
<Compile Include="Context\PerspexParserFactory.cs" />
<Compile Include="OmniXAML\Source\Glass\AutoKeyDictionary.cs" />
<Compile Include="OmniXAML\Source\Glass\ChangeTracking\ObservableProperty.cs" />
@ -228,13 +228,6 @@
<Compile Include="Parsers\SelectorParser.cs" />
<Compile Include="Parsers\SelectorGrammar.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="DataBinding\DataContextChangeSynchronizer.cs" />
<Compile Include="DataBinding\IPerspexPropertyBinder.cs" />
<Compile Include="DataBinding\PerspexPropertyBinder.cs" />
<Compile Include="DataBinding\SourceBindingEndpoint.cs" />
<Compile Include="DataBinding\TargetBindingEndpoint.cs" />
<Compile Include="DataBinding\XamlBinding.cs" />
<Compile Include="DataBinding\XamlBindingDefinition.cs" />
<Compile Include="Templates\TemplateLoader.cs" />
<Compile Include="Templates\Template.cs" />
<Compile Include="Templates\TemplateContent.cs" />
@ -244,8 +237,9 @@
<Compile Include="Context\PerspexXamlMember.cs" />
<Compile Include="Context\PerspexXamlMemberValuePlugin.cs" />
<Compile Include="Context\PerspexXamlType.cs" />
<Compile Include="Templates\XamlDataTemplate.cs" />
<Compile Include="Templates\DataTemplate.cs" />
<Compile Include="PerspexXamlLoader.cs" />
<Compile Include="Templates\TreeDataTemplate.cs" />
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
@ -289,6 +283,10 @@
<Project>{F1BAA01A-F176-4C6A-B39D-5B40BB1B148F}</Project>
<Name>Perspex.Styling</Name>
</ProjectReference>
<ProjectReference Include="..\Perspex.Markup\Perspex.Markup.csproj">
<Project>{6417e941-21bc-467b-a771-0de389353ce6}</Project>
<Name>Perspex.Markup</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Reference Include="Serilog, Version=1.5.0.0, Culture=neutral, PublicKeyToken=24c2f752a8e58a10">

26
src/Markup/Perspex.Markup.Xaml/Templates/XamlDataTemplate.cs → src/Markup/Perspex.Markup.Xaml/Templates/DataTemplate.cs

@ -8,38 +8,26 @@ using Perspex.Controls.Templates;
namespace Perspex.Markup.Xaml.Templates
{
[ContentProperty("Content")]
public class XamlDataTemplate : IDataTemplate
public class DataTemplate : IDataTemplate
{
private bool MyMatch(object data)
public Type DataType { get; set; }
public TemplateContent Content { get; set; }
public bool Match(object data)
{
if (DataType == null)
{
throw new InvalidOperationException("XAML DataTemplates must have a DataType");
throw new InvalidOperationException("DataTemplate must have a DataType.");
}
return DataType == data.GetType();
}
private Control CreateVisualTreeForItem(object data)
public IControl Build(object data)
{
var visualTreeForItem = Content.Load();
visualTreeForItem.DataContext = data;
return visualTreeForItem;
}
public Type DataType { get; set; }
public TemplateContent Content { get; set; }
public IControl Build(object param)
{
return CreateVisualTreeForItem(param);
}
public bool Match(object data)
{
return MyMatch(data);
}
}
}

53
src/Markup/Perspex.Markup.Xaml/Templates/TreeDataTemplate.cs

@ -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;
}
}
}

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

@ -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)
{
}
}
}

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

@ -0,0 +1,29 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using 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;
}
}
}

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

@ -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;
}
}
}
}

24
src/Markup/Perspex.Markup/Binding/ExpressionParseException.cs

@ -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; }
}
}

51
src/Markup/Perspex.Markup/Binding/ExpressionSubject.cs

@ -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);
}
}
}

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

@ -0,0 +1,38 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
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; }
}
}

106
src/Markup/Perspex.Markup/Binding/IndexerNode.cs

@ -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;
}
}
}

40
src/Markup/Perspex.Markup/Binding/LogicalNotNode.cs

@ -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;
}
}
}

67
src/Markup/Perspex.Markup/Binding/Parsers/ArgumentListParser.cs

@ -0,0 +1,67 @@
// Copyright (c) The Perspex Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.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;
}
}
}

125
src/Markup/Perspex.Markup/Binding/Parsers/ExpressionParser.cs

@ -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,
}
}
}

51
src/Markup/Perspex.Markup/Binding/Parsers/IdentifierParser.cs

@ -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;
}
}
}
}

34
src/Markup/Perspex.Markup/Binding/Parsers/LiteralParser.cs

@ -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;
}
}
}

44
src/Markup/Perspex.Markup/Binding/Parsers/Reader.cs

@ -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;
}
}
}
}

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

@ -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);
}
}
}
}

88
src/Markup/Perspex.Markup/Perspex.Markup.csproj

@ -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>

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

@ -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")]

8
src/Markup/Perspex.Markup/packages.config

@ -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>

2
src/Perspex.Base/BindingDescriptor.cs

@ -27,7 +27,7 @@ namespace Perspex
TwoWay,
/// <summary>
/// Copies the target to the source one time and then disposes of the binding.
/// Updates the target when the application starts or when the data context changes.
/// </summary>
OneTime,

38
src/Perspex.Base/IObservablePropertyBag.cs

@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive.Subjects;
namespace Perspex
{
@ -39,6 +40,43 @@ namespace Perspex
IObservable<T> source,
BindingPriority priority = BindingPriority.LocalValue);
/// <summary>
/// Initiates a two-way binding between <see cref="PerspexProperty"/>s.
/// </summary>
/// <param name="property">The property on this object.</param>
/// <param name="source">The source object.</param>
/// <param name="sourceProperty">The property on the source object.</param>
/// <param name="priority">The priority of the binding.</param>
/// <returns>
/// A disposable which can be used to terminate the binding.
/// </returns>
/// <remarks>
/// The binding is first carried out from <paramref name="source"/> to this.
/// </remarks>
IDisposable BindTwoWay(
PerspexProperty property,
PerspexObject source,
PerspexProperty sourceProperty,
BindingPriority priority = BindingPriority.LocalValue);
/// <summary>
/// Initiates a two-way binding between a <see cref="PerspexProperty"/> and an
/// <see cref="ISubject{Object}"/>.
/// </summary>
/// <param name="property">The property on this object.</param>
/// <param name="source">The subject to bind to.</param>
/// <param name="priority">The priority of the binding.</param>
/// <returns>
/// A disposable which can be used to terminate the binding.
/// </returns>
/// <remarks>
/// The binding is first carried out from <paramref name="source"/> to this.
/// </remarks>
IDisposable BindTwoWay(
PerspexProperty property,
ISubject<object> source,
BindingPriority priority = BindingPriority.LocalValue);
/// <summary>
/// Gets an observable for a <see cref="PerspexProperty"/>.
/// </summary>

86
src/Perspex.Base/PerspexObject.cs

@ -7,6 +7,7 @@ using System.ComponentModel;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Reflection;
using Perspex.Reactive;
using Perspex.Utilities;
@ -186,7 +187,7 @@ namespace Perspex
if (sourceBinding == null && mode > BindingMode.OneWay)
{
throw new InvalidOperationException("Can only bind OneWay to plain IObservable.");
mode = BindingMode.OneWay;
}
switch (mode)
@ -334,7 +335,7 @@ namespace Perspex
PropertyChanged -= handler;
});
},
GetObservableDescription(property));
GetDescription(property));
}
/// <summary>
@ -377,7 +378,7 @@ namespace Perspex
PropertyChanged -= handler;
});
},
GetObservableDescription(property));
GetDescription(property));
}
/// <summary>
@ -492,6 +493,7 @@ namespace Perspex
throw new ArgumentException($"The property {property.Name} is readonly.");
}
LogPropertySet(property, value, priority);
property.Setter(this, value);
}
else
@ -524,14 +526,9 @@ namespace Perspex
_values.Add(property, v);
}
LogPropertySet(property, value, priority);
v.SetValue(value, (int)priority);
}
_propertyLog.Verbose(
"Set {Property} to {$Value} with priority {Priority}",
property,
value,
priority);
}
/// <summary>
@ -557,6 +554,7 @@ namespace Perspex
throw new ArgumentException($"The property {property.Name} is readonly.");
}
LogPropertySet(property, value, priority);
property.Setter(this, value);
}
else
@ -593,14 +591,13 @@ namespace Perspex
_propertyLog.Verbose(
"Bound {Property} to {Binding} with priority LocalValue",
property,
source);
GetDescription(source));
return source.Subscribe(x => SetValue(property, x));
}
else
{
PriorityValue v;
IDescription description = source as IDescription;
if (!IsRegistered(property))
{
@ -616,7 +613,7 @@ namespace Perspex
_propertyLog.Verbose(
"Bound {Property} to {Binding} with priority {Priority}",
property,
source,
GetDescription(source),
priority);
return v.Add(source, (int)priority);
@ -658,7 +655,7 @@ namespace Perspex
}
/// <summary>
/// Initialites a two-way bind between <see cref="PerspexProperty"/>s.
/// Initiates a two-way binding between <see cref="PerspexProperty"/>s.
/// </summary>
/// <param name="property">The property on this object.</param>
/// <param name="source">The source object.</param>
@ -676,11 +673,46 @@ namespace Perspex
PerspexProperty sourceProperty,
BindingPriority priority = BindingPriority.LocalValue)
{
_propertyLog.Verbose(
"Bound two way {Property} to {Binding} with priority {Priority}",
property,
source,
priority);
return new CompositeDisposable(
Bind(property, source.GetObservable(sourceProperty)),
source.Bind(sourceProperty, GetObservable(property)));
}
/// <summary>
/// Initiates a two-way binding between a <see cref="PerspexProperty"/> and an
/// <see cref="ISubject{Object}"/>.
/// </summary>
/// <param name="property">The property on this object.</param>
/// <param name="source">The subject to bind to.</param>
/// <param name="priority">The priority of the binding.</param>
/// <returns>
/// A disposable which can be used to terminate the binding.
/// </returns>
/// <remarks>
/// The binding is first carried out from <paramref name="source"/> to this.
/// </remarks>
public IDisposable BindTwoWay(
PerspexProperty property,
ISubject<object> source,
BindingPriority priority = BindingPriority.LocalValue)
{
_propertyLog.Verbose(
"Bound two way {Property} to {Binding} with priority {Priority}",
property,
GetDescription(source),
priority);
return new CompositeDisposable(
Bind(property, source),
GetObservable(property).Subscribe(source));
}
/// <summary>
/// Forces the specified property to be revalidated.
/// </summary>
@ -926,11 +958,37 @@ namespace Perspex
/// </summary>
/// <param name="property">The property</param>
/// <returns>The description.</returns>
private string GetObservableDescription(PerspexProperty property)
private string GetDescription(PerspexProperty property)
{
return string.Format("{0}.{1}", GetType().Name, property.Name);
}
/// <summary>
/// Gets a description of an observable that van be used in logs.
/// </summary>
/// <param name="o">The observable.</param>
/// <returns>The description.</returns>
private string GetDescription(IObservable<object> o)
{
var description = o as IDescription;
return description?.Description ?? o.ToString();
}
/// <summary>
/// Logs a property set message.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="value">The new value.</param>
/// <param name="priority">The priority.</param>
private void LogPropertySet(PerspexProperty property, object value, BindingPriority priority)
{
_propertyLog.Verbose(
"Set {Property} to {$Value} with priority {Priority}",
property,
value,
priority);
}
/// <summary>
/// Throws an exception indicating that the specified property is not registered on this
/// object.

26
src/Perspex.Base/PriorityValue.cs

@ -212,25 +212,23 @@ namespace Perspex
/// <param name="priority">The priority level that the value came from.</param>
private void UpdateValue(object value, int priority)
{
if (!TypeUtilities.TryCast(_valueType, value, out value))
if (TypeUtilities.TryCast(_valueType, value, out value))
{
throw new InvalidOperationException(string.Format(
"Invalid value for Property '{0}': {1} ({2})",
_name,
value,
value?.GetType().FullName ?? "(null)"));
}
var old = _value;
var old = _value;
if (_validate != null)
{
value = _validate(value);
}
if (_validate != null)
ValuePriority = priority;
_value = value;
_changed.OnNext(Tuple.Create(old, _value));
}
else
{
value = _validate(value);
// TODO: Log error.
}
ValuePriority = priority;
_value = value;
_changed.OnNext(Tuple.Create(old, _value));
}
/// <summary>

2
src/Perspex.Controls/ContentControl.cs

@ -11,7 +11,7 @@ using Perspex.Layout;
namespace Perspex.Controls
{
/// <summary>
/// Displays <see cref="Content"/> according to a <see cref="DataTemplate"/>.
/// Displays <see cref="Content"/> according to a <see cref="FuncDataTemplate"/>.
/// </summary>
public class ContentControl : TemplatedControl, IContentControl, IReparentingHost
{

4
src/Perspex.Controls/Generators/TreeItemContainerGenerator.cs

@ -192,14 +192,14 @@ namespace Perspex.Controls.Generators
if (template == null)
{
template = DataTemplate.Default;
template = FuncDataTemplate.Default;
}
var treeTemplate = template as ITreeDataTemplate;
if (treeTemplate == null)
{
treeTemplate = new TreeDataTemplate(typeof(object), template.Build, x => null);
treeTemplate = new FuncTreeDataTemplate(typeof(object), template.Build, x => null);
}
return treeTemplate;

2
src/Perspex.Controls/IContentControl.cs

@ -7,7 +7,7 @@ namespace Perspex.Controls
{
/// <summary>
/// Defines a control that displays <see cref="Content"/> according to a
/// <see cref="Perspex.Controls.Templates.DataTemplate"/>.
/// <see cref="Perspex.Controls.Templates.FuncDataTemplate"/>.
/// </summary>
public interface IContentControl : IControl
{

2
src/Perspex.Controls/ItemsControl.cs

@ -156,8 +156,6 @@ namespace Perspex.Controls
/// <param name="e">The event args.</param>
protected virtual void ItemsChanged(PerspexPropertyChangedEventArgs e)
{
System.Diagnostics.Debug.WriteLine($"{this.GetType().Name} set items");
var incc = e.OldValue as INotifyCollectionChanged;
if (incc != null)

8
src/Perspex.Controls/Perspex.Controls.csproj

@ -72,7 +72,7 @@
<Compile Include="Primitives\ScrollInfoAdapter.cs" />
<Compile Include="Canvas.cs" />
<Compile Include="Templates\ControlTemplate`2.cs" />
<Compile Include="Templates\DataTemplate`1.cs" />
<Compile Include="Templates\FuncDataTemplate`1.cs" />
<Compile Include="Templates\FuncMemberSelector.cs" />
<Compile Include="Templates\IControlTemplate.cs" />
<Compile Include="Templates\FuncTemplate`2.cs" />
@ -102,7 +102,7 @@
<Compile Include="Controls.cs" />
<Compile Include="Templates\ControlTemplate.cs" />
<Compile Include="Templates\DataTemplateExtensions.cs" />
<Compile Include="Templates\DataTemplate.cs" />
<Compile Include="Templates\FuncDataTemplate.cs" />
<Compile Include="Generators\TreeItemContainerGenerator.cs" />
<Compile Include="Generators\ItemContainerGenerator`1.cs" />
<Compile Include="Generators\ItemContainerGenerator.cs" />
@ -129,7 +129,7 @@
<Compile Include="Templates\ITemplate`2.cs" />
<Compile Include="Templates\ITemplate`1.cs" />
<Compile Include="Templates\ITreeDataTemplate.cs" />
<Compile Include="Templates\TreeDataTemplate.cs" />
<Compile Include="Templates\FuncTreeDataTemplate.cs" />
<Compile Include="ToolTip.cs" />
<Compile Include="UserControl.cs" />
<Compile Include="Templates\TemplateExtensions.cs" />
@ -157,7 +157,7 @@
<Compile Include="TextBox.cs" />
<Compile Include="Presenters\TextPresenter.cs" />
<Compile Include="Primitives\ToggleButton.cs" />
<Compile Include="Templates\TreeDataTemplate`1.cs" />
<Compile Include="Templates\FuncTreeDataTemplate`1.cs" />
<Compile Include="TreeView.cs" />
<Compile Include="TreeViewItem.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />

2
src/Perspex.Controls/Primitives/SelectingItemsControl.cs

@ -295,8 +295,6 @@ namespace Perspex.Controls.Primitives
/// <param name="e">The event args.</param>
private void SelectedIndexChanged(PerspexPropertyChangedEventArgs e)
{
System.Diagnostics.Debug.WriteLine($"{this.GetType().Name} set selected index");
var index = (int)e.OldValue;
if (index != -1)

1
src/Perspex.Controls/Properties/AssemblyInfo.cs

@ -9,5 +9,6 @@ using Perspex.Metadata;
[assembly: InternalsVisibleTo("Perspex.Controls.UnitTests")]
[assembly: XmlnsDefinition("https://github.com/perspex", "Perspex.Controls")]
[assembly: XmlnsDefinition("https://github.com/perspex", "Perspex.Controls.Presenters")]
[assembly: XmlnsDefinition("https://github.com/perspex", "Perspex.Controls.Primitives")]
[assembly: XmlnsDefinition("https://github.com/perspex", "Perspex.Controls.Shapes")]

2
src/Perspex.Controls/Templates/DataTemplateExtensions.cs

@ -42,7 +42,7 @@ namespace Perspex.Controls.Templates
}
else
{
result = DataTemplate.Default.Build(data);
result = FuncDataTemplate.Default.Build(data);
}
result.DataContext = data;

14
src/Perspex.Controls/Templates/DataTemplate.cs → src/Perspex.Controls/Templates/FuncDataTemplate.cs

@ -9,13 +9,13 @@ namespace Perspex.Controls.Templates
/// <summary>
/// Builds a control for a piece of data.
/// </summary>
public class DataTemplate : FuncTemplate<object, IControl>, IDataTemplate
public class FuncDataTemplate : FuncTemplate<object, IControl>, IDataTemplate
{
/// <summary>
/// The default data template used in the case where not matching data template is found.
/// </summary>
public static readonly DataTemplate Default =
new DataTemplate(typeof(object), o => (o != null) ? new TextBlock { Text = o.ToString() } : null);
public static readonly FuncDataTemplate Default =
new FuncDataTemplate(typeof(object), o => (o != null) ? new TextBlock { Text = o.ToString() } : null);
/// <summary>
/// The implementation of the <see cref="Match"/> method.
@ -23,19 +23,19 @@ namespace Perspex.Controls.Templates
private readonly Func<object, bool> _match;
/// <summary>
/// Initializes a new instance of the <see cref="DataTemplate"/> class.
/// Initializes a new instance of the <see cref="FuncDataTemplate"/> class.
/// </summary>
/// <param name="type">The type of data which the data template matches.</param>
/// <param name="build">
/// A function which when passed an object of <paramref name="type"/> returns a control.
/// </param>
public DataTemplate(Type type, Func<object, IControl> build)
public FuncDataTemplate(Type type, Func<object, IControl> build)
: this(o => IsInstance(o, type), build)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="DataTemplate"/> class.
/// Initializes a new instance of the <see cref="FuncDataTemplate"/> class.
/// </summary>
/// <param name="match">
/// A function which determines whether the data template matches the specified data.
@ -43,7 +43,7 @@ namespace Perspex.Controls.Templates
/// <param name="build">
/// A function which returns a control for matching data.
/// </param>
public DataTemplate(Func<object, bool> match, Func<object, IControl> build)
public FuncDataTemplate(Func<object, bool> match, Func<object, IControl> build)
: base(build)
{
Contract.Requires<ArgumentNullException>(match != null);

10
src/Perspex.Controls/Templates/DataTemplate`1.cs → src/Perspex.Controls/Templates/FuncDataTemplate`1.cs

@ -9,21 +9,21 @@ namespace Perspex.Controls.Templates
/// Builds a control for a piece of data of specified type.
/// </summary>
/// <typeparam name="T">The type of the template's data.</typeparam>
public class DataTemplate<T> : DataTemplate
public class FuncDataTemplate<T> : FuncDataTemplate
{
/// <summary>
/// Initializes a new instance of the <see cref="DataTemplate{T}"/> class.
/// Initializes a new instance of the <see cref="FuncDataTemplate{T}"/> class.
/// </summary>
/// <param name="build">
/// A function which when passed an object of <typeparamref name="T"/> returns a control.
/// </param>
public DataTemplate(Func<T, IControl> build)
public FuncDataTemplate(Func<T, IControl> build)
: base(typeof(T), CastBuild(build))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="DataTemplate{T}"/> class.
/// Initializes a new instance of the <see cref="FuncDataTemplate{T}"/> class.
/// </summary>
/// <param name="match">
/// A function which determines whether the data template matches the specified data.
@ -31,7 +31,7 @@ namespace Perspex.Controls.Templates
/// <param name="build">
/// A function which when passed an object of <typeparamref name="T"/> returns a control.
/// </param>
public DataTemplate(Func<T, bool> match, Func<T, IControl> build)
public FuncDataTemplate(Func<T, bool> match, Func<T, IControl> build)
: base(CastMatch(match), CastBuild(build))
{
}

18
src/Perspex.Controls/Templates/TreeDataTemplate.cs → src/Perspex.Controls/Templates/FuncTreeDataTemplate.cs

@ -10,14 +10,14 @@ namespace Perspex.Controls.Templates
/// <summary>
/// A template used to build hierachical data.
/// </summary>
public class TreeDataTemplate : DataTemplate, ITreeDataTemplate
public class FuncTreeDataTemplate : FuncDataTemplate, ITreeDataTemplate
{
private readonly Func<object, IEnumerable> _itemsSelector;
private readonly Func<object, bool> _isExpanded;
/// <summary>
/// Initializes a new instance of the <see cref="TreeDataTemplate"/> class.
/// Initializes a new instance of the <see cref="FuncTreeDataTemplate"/> class.
/// </summary>
/// <param name="type">The type of data which the data template matches.</param>
/// <param name="build">
@ -27,7 +27,7 @@ namespace Perspex.Controls.Templates
/// A function which when passed an object of <paramref name="type"/> returns the child
/// items.
/// </param>
public TreeDataTemplate(
public FuncTreeDataTemplate(
Type type,
Func<object, IControl> build,
Func<object, IEnumerable> itemsSelector)
@ -36,7 +36,7 @@ namespace Perspex.Controls.Templates
}
/// <summary>
/// Initializes a new instance of the <see cref="TreeDataTemplate"/> class.
/// Initializes a new instance of the <see cref="FuncTreeDataTemplate"/> class.
/// </summary>
/// <param name="type">The type of data which the data template matches.</param>
/// <param name="build">
@ -50,7 +50,7 @@ namespace Perspex.Controls.Templates
/// A function which when passed an object of <paramref name="type"/> returns the the
/// initial expanded state of the node.
/// </param>
public TreeDataTemplate(
public FuncTreeDataTemplate(
Type type,
Func<object, IControl> build,
Func<object, IEnumerable> itemsSelector,
@ -60,7 +60,7 @@ namespace Perspex.Controls.Templates
}
/// <summary>
/// Initializes a new instance of the <see cref="TreeDataTemplate"/> class.
/// Initializes a new instance of the <see cref="FuncTreeDataTemplate"/> class.
/// </summary>
/// <param name="match">
/// A function which determines whether the data template matches the specified data.
@ -71,7 +71,7 @@ namespace Perspex.Controls.Templates
/// <param name="itemsSelector">
/// A function which when passed a matching object returns the child items.
/// </param>
public TreeDataTemplate(
public FuncTreeDataTemplate(
Func<object, bool> match,
Func<object, IControl> build,
Func<object, IEnumerable> itemsSelector)
@ -81,7 +81,7 @@ namespace Perspex.Controls.Templates
}
/// <summary>
/// Initializes a new instance of the <see cref="TreeDataTemplate"/> class.
/// Initializes a new instance of the <see cref="FuncTreeDataTemplate"/> class.
/// </summary>
/// <param name="match">
/// A function which determines whether the data template matches the specified data.
@ -96,7 +96,7 @@ namespace Perspex.Controls.Templates
/// A function which when passed a matching object returns the the initial expanded state
/// of the node.
/// </param>
public TreeDataTemplate(
public FuncTreeDataTemplate(
Func<object, bool> match,
Func<object, IControl> build,
Func<object, IEnumerable> itemsSelector,

18
src/Perspex.Controls/Templates/TreeDataTemplate`1.cs → src/Perspex.Controls/Templates/FuncTreeDataTemplate`1.cs

@ -10,10 +10,10 @@ namespace Perspex.Controls.Templates
/// A template used to build hierachical data.
/// </summary>
/// <typeparam name="T">The type of the template's data.</typeparam>
public class TreeDataTemplate<T> : TreeDataTemplate
public class FuncTreeDataTemplate<T> : FuncTreeDataTemplate
{
/// <summary>
/// Initializes a new instance of the <see cref="TreeDataTemplate{T}"/> class.
/// Initializes a new instance of the <see cref="FuncTreeDataTemplate{T}"/> class.
/// </summary>
/// <param name="build">
/// A function which when passed an object of <typeparamref name="T"/> returns a control.
@ -22,7 +22,7 @@ namespace Perspex.Controls.Templates
/// A function which when passed an object of <typeparamref name="T"/> returns the child
/// items.
/// </param>
public TreeDataTemplate(
public FuncTreeDataTemplate(
Func<T, Control> build,
Func<T, IEnumerable> itemsSelector)
: base(
@ -33,7 +33,7 @@ namespace Perspex.Controls.Templates
}
/// <summary>
/// Initializes a new instance of the <see cref="TreeDataTemplate{T}"/> class.
/// Initializes a new instance of the <see cref="FuncTreeDataTemplate{T}"/> class.
/// </summary>
/// <param name="build">
/// A function which when passed an object of <typeparamref name="T"/> returns a control.
@ -46,7 +46,7 @@ namespace Perspex.Controls.Templates
/// A function which when passed an object of <typeparamref name="T"/> returns the the
/// initial expanded state of the node.
/// </param>
public TreeDataTemplate(
public FuncTreeDataTemplate(
Func<T, Control> build,
Func<T, IEnumerable> itemsSelector,
Func<T, bool> isExpanded)
@ -59,7 +59,7 @@ namespace Perspex.Controls.Templates
}
/// <summary>
/// Initializes a new instance of the <see cref="TreeDataTemplate{T}"/> class.
/// Initializes a new instance of the <see cref="FuncTreeDataTemplate{T}"/> class.
/// </summary>
/// <param name="match">
/// A function which determines whether the data template matches the specified data.
@ -70,7 +70,7 @@ namespace Perspex.Controls.Templates
/// <param name="itemsSelector">
/// A function which when passed a matching object returns the child items.
/// </param>
public TreeDataTemplate(
public FuncTreeDataTemplate(
Func<T, bool> match,
Func<T, Control> build,
Func<T, IEnumerable> itemsSelector)
@ -82,7 +82,7 @@ namespace Perspex.Controls.Templates
}
/// <summary>
/// Initializes a new instance of the <see cref="TreeDataTemplate{T}"/> class.
/// Initializes a new instance of the <see cref="FuncTreeDataTemplate{T}"/> class.
/// </summary>
/// <param name="match">
/// A function which determines whether the data template matches the specified data.
@ -97,7 +97,7 @@ namespace Perspex.Controls.Templates
/// A function which when passed a matching object returns the the initial expanded state
/// of the node.
/// </param>
public TreeDataTemplate(
public FuncTreeDataTemplate(
Func<T, bool> match,
Func<T, Control> build,
Func<T, IEnumerable> itemsSelector,

2
src/Perspex.Controls/TextBlock.cs

@ -65,7 +65,7 @@ namespace Perspex.Controls
/// Defines the <see cref="Text"/> property.
/// </summary>
public static readonly PerspexProperty<string> TextProperty =
PerspexProperty.Register<TextBlock, string>(nameof(Text));
PerspexProperty.Register<TextBlock, string>(nameof(Text), defaultBindingMode: BindingMode.TwoWay);
/// <summary>
/// Defines the <see cref="TextAlignment"/> property.

2
src/Perspex.Diagnostics/Views/LogicalTreeView.cs

@ -48,7 +48,7 @@ namespace Perspex.Diagnostics.Views
{
DataTemplates = new DataTemplates
{
new TreeDataTemplate<LogicalTreeNode>(GetHeader, x => x.Children),
new FuncTreeDataTemplate<LogicalTreeNode>(GetHeader, x => x.Children),
},
[!ItemsControl.ItemsProperty] = this.WhenAnyValue(x => x.ViewModel.Nodes),
}),

2
src/Perspex.Diagnostics/Views/VisualTreeView.cs

@ -49,7 +49,7 @@ namespace Perspex.Diagnostics.Views
{
DataTemplates = new DataTemplates
{
new TreeDataTemplate<VisualTreeNode>(GetHeader, x => x.Children),
new FuncTreeDataTemplate<VisualTreeNode>(GetHeader, x => x.Children),
},
[!ItemsControl.ItemsProperty] = this.WhenAnyValue(x => x.ViewModel.Nodes),
}),

4
src/Perspex.Themes.Default/MenuItemStyle.cs

@ -21,8 +21,8 @@ namespace Perspex.Themes.Default
/// </summary>
public class MenuItemStyle : Styles
{
private static readonly DataTemplate AccessKeyDataTemplate =
new DataTemplate<string>(x => new AccessText { Text = x });
private static readonly FuncDataTemplate AccessKeyDataTemplate =
new FuncDataTemplate<string>(x => new AccessText { Text = x });
/// <summary>
/// Initializes a new instance of the <see cref="MenuItemStyle"/> class.

9
tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs

@ -59,14 +59,11 @@ namespace Perspex.Base.UnitTests
}
[Fact]
public void Bind_Throws_Exception_For_Invalid_Value_Type()
public void Bind_Ignores_Invalid_Value_Type()
{
Class1 target = new Class1();
Assert.Throws<InvalidOperationException>(() =>
{
target.Bind((PerspexProperty)Class1.FooProperty, Observable.Return((object)123));
});
target.Bind((PerspexProperty)Class1.FooProperty, Observable.Return((object)123));
Assert.Equal("foodefault", target.GetValue(Class1.FooProperty));
}
[Fact]

2
tests/Perspex.Controls.UnitTests/ItemsControlTests.cs

@ -305,7 +305,7 @@ namespace Perspex.Controls.UnitTests
DataContext = "Base",
DataTemplates = new DataTemplates
{
new DataTemplate<Item>(x => new Button { Content = x })
new FuncDataTemplate<Item>(x => new Button { Content = x })
},
Items = items,
};

2
tests/Perspex.Controls.UnitTests/TabControlTests.cs

@ -148,7 +148,7 @@ namespace Perspex.Controls.UnitTests
DataContext = "Base",
DataTemplates = new DataTemplates
{
new DataTemplate<Item>(x => new Button { Content = x })
new FuncDataTemplate<Item>(x => new Button { Content = x })
},
Items = items,
};

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

@ -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;
}
}
}

74
tests/Perspex.Markup.UnitTests/Binding/ExpressionNodeBuilderTests_Errors.cs

@ -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"));
}
}
}

113
tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Indexer.cs

@ -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);
}
}
}

97
tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Negation.cs

@ -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"));
}
}
}

68
tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Observable.cs

@ -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; }
}
}
}

284
tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Property.cs

@ -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
{
}
}
}

87
tests/Perspex.Markup.UnitTests/Binding/ExpressionObserverTests_Task.cs

@ -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; }
}
}
}

42
tests/Perspex.Markup.UnitTests/Binding/NotifyingBase.cs

@ -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));
}
}
}

117
tests/Perspex.Markup.UnitTests/Perspex.Markup.UnitTests.csproj

@ -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>

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

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

68
tests/Perspex.Markup.UnitTests/UnitTestSynchronizationContext.cs

@ -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();
}
}
}
}

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

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

27
tests/Perspex.Markup.Xaml.UnitTests/BinderTest.cs

@ -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));
}
}
}

131
tests/Perspex.Markup.Xaml.UnitTests/Binding/XamlBindingTests.cs

@ -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;
}
}
}

39
tests/Perspex.Markup.Xaml.UnitTests/BindingDefinitionBuilder.cs

@ -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);
}
}
}

72
tests/Perspex.Markup.Xaml.UnitTests/ChangeBranchTest.cs

@ -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…
Cancel
Save