├── .editorconfig ├── .github ├── renovate.json └── workflows │ ├── ci-build.yml │ ├── lock.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── NuGet.Config ├── README.md ├── docs ├── Performance.md ├── ReactiveMarbles.PropertyChanged.Benchmarks.BindBenchmarks-report-github.md └── ReactiveMarbles.PropertyChanged.Benchmarks.PropertyChangesBenchmarks-report-github.md ├── images └── logo.png ├── nuget.config ├── src ├── Directory.Build.props ├── Directory.build.targets ├── ReactiveMarbles.PropertyChanged.Benchmarks │ ├── .editorconfig │ ├── BindBenchmarks.cs │ ├── BindBenchmarks.tt │ ├── Directory.Build.props │ ├── Legacy │ │ ├── BindExtensions.cs │ │ ├── ExpressionExtensions.cs │ │ ├── GetMemberFuncCache.cs │ │ ├── NotifyPropertyChangedExtensions.cs │ │ └── SetMemberFuncCache.cs │ ├── Moqs │ │ └── TestClass.cs │ ├── Program.cs │ ├── ReactiveMarbles.PropertyChanged.Benchmarks.csproj │ ├── WhenChangedBenchmarks.cs │ └── WhenChangedBenchmarks.tt ├── ReactiveMarbles.PropertyChanged.SourceGenerator.Benchmarks │ ├── .editorconfig │ ├── Directory.Build.props │ ├── Program.cs │ ├── ReactiveMarbles.PropertyChanged.SourceGenerator.Benchmarks.csproj │ ├── WhenChangedBenchmarks.cs │ └── WhenChangedBenchmarks.tt ├── ReactiveMarbles.PropertyChanged.SourceGenerator.Builders │ ├── AccessibilityExtensions.cs │ ├── BaseUserSourceBuilder.cs │ ├── BindHostBuilder.cs │ ├── BindHostProxy.cs │ ├── CompilationUtil.cs │ ├── EmptyClassBuilder.cs │ ├── HostProxyBase.cs │ ├── InvocationKind.cs │ ├── MethodNames.cs │ ├── ReactiveMarbles.PropertyChanged.SourceGenerator.Builders.csproj │ ├── ReceiverKind.cs │ ├── ReflectionUtil.cs │ ├── WhenChangedHostBuilder.cs │ └── WhenChangedHostProxy.cs ├── ReactiveMarbles.PropertyChanged.SourceGenerator.Sample │ ├── OtherNamespace │ │ └── SampleClass.cs │ ├── Program.cs │ ├── ReactiveMarbles.PropertyChanged.SourceGenerator.Sample.csproj │ ├── SampleClass.cs │ └── SampleClass2.cs ├── ReactiveMarbles.PropertyChanged.SourceGenerator.Tests │ ├── AccessibilityTestCases.cs │ ├── AccessibilityTestCases.tt │ ├── BindFixture.cs │ ├── BindGeneratorTestsDiagnostics.cs │ ├── BindGeneratorTestsNoDiagnostics.cs │ ├── CommonTest.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── ReactiveMarbles.PropertyChanged.SourceGenerator.Tests.csproj │ ├── SampleTests.cs │ ├── TestHelper.cs │ ├── WhenChangedFixture.cs │ ├── WhenChangedGeneratorTestsDiagnostics.cs │ ├── WhenChangedGeneratorTestsNoDiagnostics.cs │ ├── WhenChangingGeneratorTestsDiagnostics.cs │ ├── WhenChangingGeneratorTestsNoDiagnostics.cs │ └── xunit.runner.json ├── ReactiveMarbles.PropertyChanged.SourceGenerator │ ├── AnalyzerReleases.Shipped.md │ ├── AnalyzerReleases.Unshipped.md │ ├── Comparers │ │ └── SyntaxNodeComparer.cs │ ├── Constants.Binding.cs │ ├── Constants.When.cs │ ├── Constants.When.tt │ ├── Constants.cs │ ├── DiagnosticWarnings.cs │ ├── Generator.cs │ ├── Helpers │ │ ├── AccessibilityExtensions.cs │ │ ├── Extensions.cs │ │ ├── GeneratorHelpers.cs │ │ ├── RoslynExtensions.cs │ │ └── SourceHelpers.cs │ ├── IsExternalInit.cs │ ├── MethodCreators │ │ ├── MethodCreator.BindCommon.cs │ │ ├── MethodCreator.BindOneWay.cs │ │ ├── MethodCreator.BindTwoWay.cs │ │ ├── MethodCreator.WhenCommon.cs │ │ ├── MethodCreator.cs │ │ └── Transient │ │ │ ├── BindStatementsDatum.cs │ │ │ ├── ClassDatum.cs │ │ │ ├── CompilationDatum.cs │ │ │ ├── ExpressionArgument.cs │ │ │ ├── ExpressionChain.cs │ │ │ ├── MethodDatum.cs │ │ │ ├── MultiWhenStatementsDatum.cs │ │ │ ├── NamespaceDatum.cs │ │ │ └── WhenStatementsDatum.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── ReactiveMarbles - Backup.PropertyChanged.SourceGenerator.csproj │ ├── ReactiveMarbles.PropertyChanged.SourceGenerator.csproj │ ├── ReactiveMarbles.PropertyChanged.SourceGenerator.csproj.DotSettings │ ├── SyntaxReceiver.cs │ └── TypeSymbolComparer.cs ├── ReactiveMarbles.PropertyChanged.Tests │ ├── BindTests.cs │ ├── Moqs │ │ ├── A.cs │ │ ├── B.cs │ │ ├── BaseTestClass.cs │ │ └── C.cs │ ├── ReactiveMarbles.PropertyChanged.Tests.csproj │ └── WhenChangedTests.cs ├── ReactiveMarbles.PropertyChanged.sln ├── ReactiveMarbles.PropertyChanged │ ├── BindExtensions.cs │ ├── ExpressionExtensions.cs │ ├── GetMemberFuncCache.cs │ ├── MemberFuncCacheKeyComparer.cs │ ├── NotifyPropertiesChangeExtensions.cs │ ├── NotifyPropertiesChangeExtensions.tt │ ├── NotifyPropertyChangedExtensions.cs │ └── ReactiveMarbles.PropertyChanged.csproj ├── global.json └── stylecop.json └── version.json /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["local>reactivemarbles/.github:renovate"] 4 | } -------------------------------------------------------------------------------- /.github/workflows/ci-build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | configuration: Release 11 | productNamespacePrefix: "ReactiveMarbles" 12 | 13 | jobs: 14 | build: 15 | runs-on: windows-2022 16 | outputs: 17 | nbgv: ${{ steps.nbgv.outputs.SemVer2 }} 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4.2.2 21 | with: 22 | fetch-depth: 0 23 | lfs: true 24 | 25 | - name: Install .NET 6 and 7 26 | uses: actions/setup-dotnet@v4.3.1 27 | with: 28 | dotnet-version: | 29 | 6.0.x 30 | 7.0.x 31 | 32 | - uses: nuget/setup-nuget@v2 33 | name: Setup NuGet 34 | 35 | - name: Install DotNet workloads 36 | shell: bash 37 | run: | 38 | dotnet workload install android 39 | dotnet workload install ios 40 | dotnet workload install tvos 41 | dotnet workload install macos 42 | dotnet workload install maui 43 | dotnet workload install maccatalyst 44 | 45 | - name: Add MSBuild to PATH 46 | uses: glennawatson/setup-msbuild@v1.0.3 47 | with: 48 | prerelease: true 49 | 50 | - name: NBGV 51 | id: nbgv 52 | uses: dotnet/nbgv@master 53 | with: 54 | setAllVars: true 55 | 56 | - name: NuGet Restore 57 | run: nuget restore 58 | working-directory: src 59 | 60 | - name: Build 61 | run: msbuild /t:build,pack /nowarn:MSB4011 /maxcpucount /p:NoPackageAnalysis=true /verbosity:minimal /p:Configuration=${{ env.configuration }} 62 | working-directory: src 63 | 64 | 65 | - name: Run Unit Tests and Generate Coverage 66 | uses: glennawatson/coverlet-msbuild@v2 67 | with: 68 | project-files: '**/*Tests*.csproj' 69 | no-build: true 70 | exclude-filter: '[${{env.productNamespacePrefix}}.*.Tests.*]*' 71 | include-filter: '[${{env.productNamespacePrefix}}*]*' 72 | output-format: cobertura 73 | output: '../../artifacts/' 74 | configuration: ${{ env.configuration }} 75 | 76 | - name: Upload Code Coverage 77 | shell: bash 78 | run: | 79 | echo $PWD 80 | bash <(curl -s https://codecov.io/bash) -X gcov -X coveragepy -t ${{ env.CODECOV_TOKEN }} -s '$PWD/artifacts' -f '*.xml' 81 | env: 82 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 83 | 84 | - name: Create NuGet Artifacts 85 | uses: actions/upload-artifact@master 86 | with: 87 | name: nuget 88 | path: '**/*.nupkg' 89 | -------------------------------------------------------------------------------- /.github/workflows/lock.yml: -------------------------------------------------------------------------------- 1 | name: 'Lock Threads' 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | 12 | concurrency: 13 | group: lock 14 | 15 | jobs: 16 | action: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: dessant/lock-threads@v5 20 | with: 21 | github-token: ${{ github.token }} 22 | issue-inactive-days: '14' 23 | pr-inactive-days: '14' 24 | issue-comment: > 25 | This issue has been automatically locked since there 26 | has not been any recent activity after it was closed. 27 | Please open a new issue for related bugs. 28 | pr-comment: > 29 | This pull request has been automatically locked since there 30 | has not been any recent activity after it was closed. 31 | Please open a new issue for related bugs. -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | env: 8 | configuration: Release 9 | productNamespacePrefix: "ReactiveMarbles" 10 | 11 | jobs: 12 | build: 13 | runs-on: windows-2022 14 | environment: 15 | name: release 16 | outputs: 17 | nbgv: ${{ steps.nbgv.outputs.SemVer2 }} 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4.2.2 21 | with: 22 | fetch-depth: 0 23 | lfs: true 24 | 25 | - name: Install .NET 6 26 | uses: actions/setup-dotnet@v4.3.1 27 | with: 28 | dotnet-version: 6.0.x 29 | include-prerelease: true 30 | 31 | - uses: nuget/setup-nuget@v2 32 | name: Setup NuGet 33 | 34 | - name: Install DotNet workloads 35 | shell: bash 36 | run: | 37 | dotnet workload install android 38 | dotnet workload install ios 39 | dotnet workload install tvos 40 | dotnet workload install macos 41 | dotnet workload install maui 42 | dotnet workload install maccatalyst 43 | 44 | - name: Add MSBuild to PATH 45 | uses: glennawatson/setup-msbuild@v1.0.3 46 | with: 47 | prerelease: true 48 | 49 | - name: NBGV 50 | id: nbgv 51 | uses: dotnet/nbgv@master 52 | with: 53 | setAllVars: true 54 | 55 | - name: NuGet Restore 56 | run: nuget restore 57 | working-directory: src 58 | 59 | - name: Build 60 | run: msbuild /t:build,pack /nowarn:MSB4011 /maxcpucount /p:NoPackageAnalysis=true /verbosity:minimal /p:Configuration=${{ env.configuration }} 61 | working-directory: src 62 | 63 | # Decode the base 64 encoded pfx and save the Signing_Certificate 64 | - name: Sign NuGet packages 65 | shell: pwsh 66 | run: | 67 | $pfx_cert_byte = [System.Convert]::FromBase64String("${{ secrets.SIGNING_CERTIFICATE }}") 68 | [IO.File]::WriteAllBytes("GitHubActionsWorkflow.pfx", $pfx_cert_byte) 69 | $secure_password = ConvertTo-SecureString ${{ secrets.SIGN_CERTIFICATE_PASSWORD }} –asplaintext –force 70 | Import-PfxCertificate -FilePath GitHubActionsWorkflow.pfx -Password $secure_password -CertStoreLocation Cert:\CurrentUser\My 71 | nuget sign -Timestamper http://timestamp.digicert.com -CertificateFingerprint ${{ secrets.SIGN_CERTIFICATE_HASH }} **/*.nupkg 72 | 73 | - name: Changelog 74 | uses: glennawatson/ChangeLog@v1.1 75 | id: changelog 76 | 77 | - name: Create Release 78 | uses: actions/create-release@v1.1.4 79 | env: 80 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token 81 | with: 82 | tag_name: ${{ steps.nbgv.outputs.SemVer2 }} 83 | release_name: ${{ steps.nbgv.outputs.SemVer2 }} 84 | body: | 85 | ${{ steps.changelog.outputs.commitLog }} 86 | 87 | - name: NuGet Push 88 | env: 89 | NUGET_AUTH_TOKEN: ${{ secrets.NUGET_API_KEY }} 90 | SOURCE_URL: https://api.nuget.org/v3/index.json 91 | run: | 92 | dotnet nuget push -s ${{ env.SOURCE_URL }} -k ${{ env.NUGET_AUTH_TOKEN }} **/*.nupkg 93 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2021 ReactiveUI Association Incorporated 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NuGet.Config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reactive Marbles Property Changed 2 | 3 | ## Packages 4 | 5 | | Container | NuGet 6 | |---------|-------| 7 | | [PropertyChanged][PropertyChanged] | [![PropertyChangedBadge]][PropertyChanged] 8 | | [PropertyChanged.SourceGenerator][PropertyChangedSourceGen] | [![PropertyChangedSourceGenBadge]][PropertyChangedSourceGen] 9 | 10 | [PropertyChanged]: https://www.nuget.org/packages/ReactiveMarbles.PropertyChanged/ 11 | [PropertyChangedBadge]: https://img.shields.io/nuget/v/ReactiveMarbles.PropertyChanged.svg 12 | [PropertyChangedSourceGen]: https://www.nuget.org/packages/ReactiveMarbles.PropertyChanged.SourceGenerator/ 13 | [PropertyChangedSourceGenBadge]: https://img.shields.io/nuget/v/ReactiveMarbles.PropertyChanged.SourceGenerator.svg 14 | 15 | ## Overview 16 | 17 | A framework for providing an observable with the latest value of a property expression. 18 | 19 | The source generator version will generate raw source code for the binding. If you have private/protected classes and/or properties it may require partial classes. 20 | 21 | The regular version will use Expression trees on platforms that support it (no iOS based platforms). On iOS it will just use reflection. This provides a roughly 2x performance boost for those platforms that can use expression trees. 22 | 23 | ```cs 24 | this.WhenChanged(x => x.Property1.Property2.Property3); 25 | ``` 26 | 27 | The above will generate a `IObservable` where T is the type of `Property3`. It will signal each time a value has changed. It is aware of all property changes in the property chain. 28 | 29 | ## Binding 30 | 31 | There are several methods of binding. 32 | 33 | First is two way binding. Two way binding will update either the `host` or the `target` whenever the target property has changed. 34 | 35 | ```cs 36 | host.BindTwoWay(target, host => host.B.C, target => target.D.E); 37 | ``` 38 | 39 | One way binding will only update the `target` with changes the `host`'s specified target property. 40 | 41 | ```cs 42 | host.BindOneWay(target, host => host.B.C); 43 | ``` 44 | 45 | There are also overloads with lambdas that allow you to convert from the `host` to the `target`. These will allow you to convert at binding time to the specified formats. 46 | 47 | ```cs 48 | host.BindOneWay(target, host => host.B.C, hostProp => ConvertToTargetPropType(hostProp)); 49 | host.BindTwoWay(target, host => host.B.C, target => target.D.E, hostProp => ConvertToTargetPropType(hostProp), targetProp => ConvertToHostPropType(targetProp)); 50 | ``` 51 | 52 | # Limitations compared to ReactiveUI 53 | 54 | At the moment it only supports `INotifyPropertyChanged` properties. More property types to come such as WPF DependencyProperty. 55 | 56 | # Milestones 57 | 58 | * Implement initial binding and property changes. 59 | 60 | # Benchmark Comparisons 61 | 62 | Detailed benchmarking results can be found [here](/docs/Performance.md). 63 | -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reactivemarbles/PropertyChanged/f9c06ab9694384fd1fb37f3b13902434bff2b2bd/images/logo.png -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | AnyCPU 5 | $(MSBuildProjectName.Contains('Tests')) 6 | Glenn Watson 7 | Copyright (c) 2021 ReactiveUI Association Incorporated 8 | logo.png 9 | MIT 10 | https://github.com/reactivemarbles/PropertyChanged 11 | Allows to get an observables for property changed events. 12 | glennawatson 13 | system.reactive;propertychanged;inpc;reactive;functional 14 | https://github.com/reactivemarbles/PropertyChanged/releases 15 | https://github.com/reactivemarbles/PropertyChanged 16 | git 17 | true 18 | true 19 | 20 | 21 | true 22 | 23 | true 24 | 25 | $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb 26 | True 27 | latest 28 | 29 | 30 | 31 | true 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | $(MSBuildThisFileDirectory) 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/Directory.build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | $(AssemblyName) ($(TargetFramework)) 4 | 5 | 6 | 7 | $(DefineConstants);NET_45;XAML 8 | 9 | 10 | $(DefineConstants);NETFX_CORE;XAML;WINDOWS_UWP 11 | 12 | 13 | $(DefineConstants);MONO;UIKIT;COCOA 14 | 15 | 16 | $(DefineConstants);MONO;COCOA 17 | 18 | 19 | $(DefineConstants);MONO;UIKIT;COCOA 20 | 21 | 22 | $(DefineConstants);MONO;UIKIT;COCOA 23 | 24 | 25 | $(DefineConstants);MONO;ANDROID 26 | 27 | 28 | $(DefineConstants);TIZEN 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.Benchmarks/.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | # override for benchmarks. 3 | 4 | # top-most EditorConfig file 5 | root = true -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.Benchmarks/BindBenchmarks.tt: -------------------------------------------------------------------------------- 1 | <#@ template debug="false" hostspecific="false" language="C#" #> 2 | <#@ assembly name="System.Core.dll" #> 3 | <#@ assembly name="System.Collections.dll" #> 4 | <#@ import namespace="System.Linq" #> 5 | <#@ output extension=".cs" #> 6 | // Copyright (c) 2019-2020 ReactiveUI Association Incorporated. All rights reserved. 7 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 8 | // See the LICENSE file in the project root for full license information. 9 | 10 | using System; 11 | using BenchmarkDotNet.Attributes; 12 | using BenchmarkDotNet.Configs; 13 | using BenchmarkDotNet.Jobs; 14 | 15 | using ReactiveMarbles.PropertyChanged.Benchmarks.Moqs; 16 | <# 17 | // First entry will be classified as the baseline. 18 | var participants = new (string Alias, string MethodName, string FullClassName)[] 19 | { 20 | ("UI", "Bind", "ReactiveUI.PropertyBindingMixins"), 21 | ("Old", "Bind", "ReactiveMarbles.PropertyChanged.Benchmarks.Legacy.BindExtensions"), 22 | ("New", "Bind", "ReactiveMarbles.PropertyChanged.BindExtensions"), 23 | }; 24 | var depths = new[] { 1, 2, 3 }; 25 | const string BindAndChange = "BindAndChange"; 26 | const string Change = "Change"; 27 | string GetBenchmarkName(string baseName, int depth, string alias) => $"{baseName}_Depth{depth}_{alias}"; 28 | 29 | foreach(var (Alias, MethodName, FullClassName) in participants) 30 | { 31 | #> 32 | using <#= Alias #> = <#= FullClassName #>; 33 | <#}#> 34 | 35 | namespace ReactiveMarbles.PropertyChanged.Benchmarks 36 | { 37 | /// 38 | /// Benchmarks for binding. 39 | /// 40 | [SimpleJob(RuntimeMoniker.NetCoreApp31)] 41 | [MemoryDiagnoser] 42 | [MarkdownExporterAttribute.GitHub] 43 | [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] 44 | public class BindBenchmarks 45 | { 46 | private TestClass _from; 47 | private TestClass _to; 48 | private IDisposable _binding; 49 | 50 | /// 51 | /// The number mutations to perform. 52 | /// 53 | [Params(1, 10, 100, 1000)] 54 | public int Changes; 55 | 56 | <# 57 | for(var i = 0; i < depths.Length; i++) 58 | { 59 | var depth = depths[i]; #> 60 | [GlobalSetup(Targets = new[] { <#= string.Join(", ", participants.Select(x => $"\"{GetBenchmarkName(BindAndChange, depth, x.Alias)}\"")) #> })] 61 | public void Depth<#= depth #>Setup() 62 | { 63 | _from = new TestClass(<#= depth #>); 64 | _to = new TestClass(<#= depth #>); 65 | } 66 | 67 | <# } #> 68 | public void PerformMutations(int depth) 69 | { 70 | // We loop through the changes, alternating mutations to the source and destination at every depth. 71 | var d2 = depth * 2; 72 | for (var i = 0; i < Changes; ++i) 73 | { 74 | var a = i % d2; 75 | var t = (a % 2) > 0 ? _to : _from; 76 | t.Mutate(a / 2); 77 | } 78 | } 79 | 80 | <# 81 | for(var i = 0; i < depths.Length; i++) 82 | { 83 | var depth = depths[i]; 84 | for (var j = 0; j < participants.Length; j++) 85 | { 86 | var (Alias, MethodName, FullClassName) = participants[j]; #> 87 | [BenchmarkCategory("Bind and Change Depth <#= depth #>")] 88 | <# if (j == 0) WriteLine("[Benchmark(Baseline = true)]"); else WriteLine("[Benchmark]"); #> 89 | public void <#= GetBenchmarkName(BindAndChange, depth, Alias) #>() 90 | { 91 | <# 92 | var expression = string.Join(".", Enumerable.Range(1, depth - 1).Select(x => "Child").Prepend("x").Append("Value")); #> 93 | using var binding = <#= $"{Alias}.{MethodName}(_from, _to, x => {expression}, x => {expression});" #> 94 | PerformMutations(<#= depth #>); 95 | } 96 | 97 | <# } #> 98 | <# } #> 99 | <# 100 | for(var i = 0; i < depths.Length; i++) 101 | { 102 | var depth = depths[i]; 103 | for (var j = 0; j < participants.Length; j++) 104 | { 105 | var (Alias, MethodName, FullClassName) = participants[j]; 106 | var benchmarkName = GetBenchmarkName(Change, depth, Alias); #> 107 | [GlobalSetup(Target = <#= $"\"{benchmarkName}\"" #>)] 108 | public void <#= $"{benchmarkName}Setup" #>() 109 | { 110 | <# 111 | var expression = string.Join(".", Enumerable.Range(1, depth - 1).Select(x => "Child").Prepend("x").Append("Value")); #> 112 | Depth<#= depth #>Setup(); 113 | _binding = <#= $"{Alias}.{MethodName}(_from, _to, x => {expression}, x => {expression});" #> 114 | } 115 | 116 | [BenchmarkCategory("Change Depth <#= depth #>")] 117 | <# if (j == 0) WriteLine("[Benchmark(Baseline = true)]"); else WriteLine("[Benchmark]"); #> 118 | public void <#= benchmarkName #>() 119 | { 120 | PerformMutations(<#= depth #>); 121 | } 122 | 123 | <# } #> 124 | <# 125 | } #> 126 | [GlobalCleanup(Targets = new[] { <#= string.Join(", ", depths.SelectMany(depth => participants.Select(x => $"\"{GetBenchmarkName(Change, depth, x.Alias)}\""))) #> })] 127 | public void GlobalCleanup() 128 | { 129 | _binding.Dispose(); 130 | } 131 | } 132 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.Benchmarks/Directory.Build.props: -------------------------------------------------------------------------------- 1 |  2 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.Benchmarks/Legacy/ExpressionExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2020 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Linq.Expressions; 9 | 10 | namespace ReactiveMarbles.PropertyChanged.Benchmarks.Legacy 11 | { 12 | internal static class ExpressionExtensions 13 | { 14 | internal static object GetParentForExpression(this List> chain, object startItem) 15 | { 16 | object current = startItem; 17 | foreach (Func valueFetcher in chain) 18 | { 19 | current = valueFetcher.Invoke(current); 20 | } 21 | 22 | return current; 23 | } 24 | 25 | internal static List> GetGetValueMemberChain(this Expression expression) 26 | { 27 | List> returnValue = new List>(); 28 | List expressionChain = expression.GetExpressionChain(); 29 | 30 | foreach (MemberExpression value in expressionChain.Take(expressionChain.Count - 1)) 31 | { 32 | Func valueFetcher = GetMemberFuncCache.GetCache(value.Member); 33 | 34 | returnValue.Add(valueFetcher); 35 | } 36 | 37 | return returnValue; 38 | } 39 | 40 | internal static List GetExpressionChain(this Expression expression) 41 | { 42 | List expressions = new List(16); 43 | 44 | Expression node = expression; 45 | 46 | while (node.NodeType != ExpressionType.Parameter) 47 | { 48 | switch (node.NodeType) 49 | { 50 | case ExpressionType.MemberAccess: 51 | MemberExpression memberExpression = (MemberExpression)node; 52 | expressions.Add(memberExpression); 53 | node = memberExpression.Expression; 54 | break; 55 | default: 56 | throw new NotSupportedException($"Unsupported expression type: '{node.NodeType}'"); 57 | } 58 | } 59 | 60 | expressions.Reverse(); 61 | 62 | return expressions; 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.Benchmarks/Legacy/GetMemberFuncCache.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2020 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Collections.Concurrent; 7 | using System.Diagnostics.CodeAnalysis; 8 | using System.Linq.Expressions; 9 | using System.Reflection; 10 | 11 | namespace ReactiveMarbles.PropertyChanged.Benchmarks.Legacy 12 | { 13 | internal static class GetMemberFuncCache 14 | { 15 | #if !UIKIT 16 | private static readonly 17 | ConcurrentDictionary<(Type FromType, string MemberName), Func> Cache 18 | = new ConcurrentDictionary<(Type, string), Func>(); 19 | #endif 20 | 21 | [SuppressMessage("Design", "CA1801: Parameter not used", Justification = "Used on some platforms")] 22 | public static Func GetCache(MemberInfo memberInfo) 23 | { 24 | #if UIKIT 25 | switch (memberInfo) 26 | { 27 | case PropertyInfo propertyInfo: 28 | return input => (TReturn)propertyInfo.GetValue(input); 29 | case FieldInfo fieldInfo: 30 | return input => (TReturn)fieldInfo.GetValue(input); 31 | default: 32 | throw new ArgumentException($"Cannot handle member {memberInfo.Name}", nameof(memberInfo)); 33 | } 34 | #else 35 | return Cache.GetOrAdd((memberInfo.DeclaringType, memberInfo.Name), _ => 36 | { 37 | ParameterExpression instance = Expression.Parameter(typeof(TFrom), "instance"); 38 | 39 | UnaryExpression castInstance = Expression.Convert(instance, memberInfo.DeclaringType); 40 | 41 | Expression body; 42 | 43 | switch (memberInfo) 44 | { 45 | case PropertyInfo propertyInfo: 46 | body = Expression.Call(castInstance, propertyInfo.GetGetMethod()); 47 | break; 48 | case FieldInfo fieldInfo: 49 | body = Expression.Field(castInstance, fieldInfo); 50 | break; 51 | default: 52 | throw new ArgumentException($"Cannot handle member {memberInfo.Name}", nameof(memberInfo)); 53 | } 54 | 55 | ParameterExpression[] parameters = new[] { instance }; 56 | 57 | Expression> lambdaExpression = Expression.Lambda>(body, parameters); 58 | 59 | return lambdaExpression.Compile(); 60 | }); 61 | #endif 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.Benchmarks/Legacy/NotifyPropertyChangedExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2020 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.ComponentModel; 7 | using System.Linq.Expressions; 8 | using System.Reactive.Linq; 9 | 10 | namespace ReactiveMarbles.PropertyChanged.Benchmarks.Legacy 11 | { 12 | /// 13 | /// Provides extension methods for the notify property changed extensions. 14 | /// 15 | public static class NotifyPropertyChangedExtensions 16 | { 17 | /// 18 | /// Notifies when the specified property changes. 19 | /// 20 | /// The object to monitor. 21 | /// The expression to the object. 22 | /// The type of initial object. 23 | /// The eventual return value. 24 | /// An observable that signals when the property specified in the expression has changed. 25 | /// Either the property expression or the object to monitor is null. 26 | /// If there is an issue with the property expression. 27 | public static IObservable WhenChanged( 28 | this TObj objectToMonitor, 29 | Expression> propertyExpression) 30 | where TObj : class, INotifyPropertyChanged 31 | { 32 | if (propertyExpression == null) 33 | { 34 | throw new ArgumentNullException(nameof(propertyExpression)); 35 | } 36 | 37 | return WhenPropertyChanges(objectToMonitor, propertyExpression).Select(x => x.Value); 38 | } 39 | 40 | /// 41 | /// Notifies when the specified property changes. 42 | /// 43 | /// The object to monitor. 44 | /// The expression to the object. 45 | /// The type of initial object. 46 | /// The eventual return value. 47 | /// An observable that signals when the property specified in the expression has changed. 48 | /// Either the property expression or the object to monitor is null. 49 | /// If there is an issue with the property expression. 50 | public static IObservable<(object Sender, TReturn Value)> WhenPropertyChanges( 51 | this TObj objectToMonitor, 52 | Expression> propertyExpression) 53 | where TObj : class, INotifyPropertyChanged 54 | { 55 | if (propertyExpression == null) 56 | { 57 | throw new ArgumentNullException(nameof(propertyExpression)); 58 | } 59 | 60 | IObservable<(object Sender, INotifyPropertyChanged Value)> currentObservable = Observable.Return(((object)null, (INotifyPropertyChanged)objectToMonitor)); 61 | 62 | System.Collections.Generic.List expressionChain = propertyExpression.Body.GetExpressionChain(); 63 | 64 | if (expressionChain.Count == 0) 65 | { 66 | throw new ArgumentException("There are no fields in the expressions", nameof(propertyExpression)); 67 | } 68 | 69 | int i = 0; 70 | foreach (MemberExpression memberExpression in expressionChain) 71 | { 72 | if (i == expressionChain.Count - 1) 73 | { 74 | return currentObservable 75 | .Where(parent => parent.Value != null) 76 | .Select(parent => GenerateObservable(parent.Value, memberExpression)) 77 | .Switch(); 78 | } 79 | 80 | currentObservable = currentObservable 81 | .Where(parent => parent.Value != null) 82 | .Select(parent => GenerateObservable(parent.Value, memberExpression)) 83 | .Switch(); 84 | 85 | i++; 86 | } 87 | 88 | throw new ArgumentException("Invalid expression", nameof(propertyExpression)); 89 | } 90 | 91 | private static IObservable<(object Sender, T Value)> GenerateObservable(INotifyPropertyChanged parent, MemberExpression memberExpression) 92 | { 93 | System.Reflection.MemberInfo memberInfo = memberExpression.Member; 94 | string memberName = memberInfo.Name; 95 | 96 | Func func = GetMemberFuncCache.GetCache(memberInfo); 97 | return Observable.FromEvent( 98 | handler => 99 | { 100 | void Handler(object sender, PropertyChangedEventArgs e) 101 | { 102 | handler((sender, e)); 103 | } 104 | 105 | return Handler; 106 | }, 107 | x => parent.PropertyChanged += x, 108 | x => parent.PropertyChanged -= x) 109 | .Where(x => x.Args.PropertyName == memberName) 110 | .Select(x => (x.Sender, func.Invoke(parent))) 111 | .StartWith((parent, func.Invoke(parent))); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.Benchmarks/Legacy/SetMemberFuncCache.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2020 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Collections.Concurrent; 7 | using System.Diagnostics.CodeAnalysis; 8 | using System.Linq.Expressions; 9 | using System.Reflection; 10 | 11 | namespace ReactiveMarbles.PropertyChanged.Benchmarks.Legacy 12 | { 13 | internal static class SetMemberFuncCache 14 | { 15 | #if !UIKIT 16 | private static readonly 17 | ConcurrentDictionary<(Type ParentType, string PropertyName), Action> Cache 18 | = new ConcurrentDictionary<(Type, string), Action>(); 19 | #endif 20 | 21 | [SuppressMessage("Design", "CA1801: Parameter not used", Justification = "Used on some platforms")] 22 | public static Action GenerateSetCache(MemberInfo memberInfo) 23 | { 24 | #if UIKIT 25 | switch (memberInfo) 26 | { 27 | case PropertyInfo propertyInfo: 28 | return (input, value) => propertyInfo.SetValue(input, value); 29 | case FieldInfo fieldInfo: 30 | return (input, value) => fieldInfo.SetValue(input, value); 31 | default: 32 | throw new ArgumentException($"Cannot handle member {memberInfo.Name}", nameof(memberInfo)); 33 | } 34 | #else 35 | return Cache.GetOrAdd((memberInfo.DeclaringType, memberInfo.Name), _ => 36 | { 37 | ParameterExpression instance = Expression.Parameter(typeof(object), "instance"); 38 | ParameterExpression valueParam = Expression.Parameter(typeof(TValue), "property"); 39 | 40 | Expression body; 41 | 42 | switch (memberInfo) 43 | { 44 | case PropertyInfo propertyInfo: 45 | UnaryExpression convertProp = Expression.Convert(valueParam, propertyInfo.PropertyType); 46 | UnaryExpression convertInstanceProp = Expression.Convert(instance, propertyInfo.DeclaringType); 47 | body = Expression.Call(convertInstanceProp, propertyInfo.GetSetMethod(), convertProp); 48 | break; 49 | case FieldInfo fieldInfo: 50 | UnaryExpression convertInstanceField = Expression.Convert(instance, fieldInfo.DeclaringType); 51 | MemberExpression field = Expression.Field(convertInstanceField, fieldInfo); 52 | UnaryExpression convertField = Expression.Convert(valueParam, fieldInfo.FieldType); 53 | body = Expression.Assign(field, convertField); 54 | break; 55 | default: 56 | throw new ArgumentException($"Cannot handle member {memberInfo.Name}", nameof(memberInfo)); 57 | } 58 | 59 | ParameterExpression[] parameters = new[] { instance, valueParam }; 60 | 61 | Expression> lambdaExpression = Expression.Lambda>(body, parameters); 62 | 63 | return lambdaExpression.Compile(); 64 | }); 65 | #endif 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.Benchmarks/Moqs/TestClass.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2020 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Collections.Concurrent; 7 | using System.Collections.Generic; 8 | using System.ComponentModel; 9 | using System.Linq.Expressions; 10 | using System.Reflection; 11 | using System.Runtime.CompilerServices; 12 | 13 | using ReactiveUI; 14 | 15 | namespace ReactiveMarbles.PropertyChanged.Benchmarks.Moqs 16 | { 17 | public class TestClass : INotifyPropertyChanged, IViewFor 18 | { 19 | private static readonly PropertyInfo _childPropertyInfo = typeof(TestClass).GetProperty("Child"); 20 | private static readonly PropertyInfo _valuePropertyInfo = typeof(TestClass).GetProperty("Value"); 21 | 22 | private static readonly ConcurrentDictionary>> _getValueExpression 23 | = new ConcurrentDictionary>>(); 24 | private TestClass _child; 25 | private int _value; 26 | 27 | public readonly int Height; 28 | 29 | public TestClass(int height = 1) 30 | { 31 | if (height < 1) 32 | { 33 | height = 1; 34 | } 35 | 36 | Height = height; 37 | if (height > 1) 38 | { 39 | Child = new TestClass(height - 1); 40 | } 41 | } 42 | 43 | public event PropertyChangedEventHandler PropertyChanged; 44 | 45 | public TestClass Child 46 | { 47 | get => _child; 48 | set => RaiseAndSetIfChanged(ref _child, value); 49 | } 50 | 51 | public int Value 52 | { 53 | get => _value; 54 | set => RaiseAndSetIfChanged(ref _value, value); 55 | } 56 | 57 | public void Mutate(int depth = 0) 58 | { 59 | if (depth >= Height) 60 | { 61 | throw new ArgumentOutOfRangeException(nameof(depth)); 62 | } 63 | 64 | int height = Height; 65 | TestClass current = this; 66 | while (--height > depth) 67 | { 68 | current = current?.Child; 69 | } 70 | 71 | if (height < 1 && current != null) 72 | { 73 | // We're at the bottom, so tweak the value 74 | current.Value++; 75 | return; 76 | } 77 | 78 | // Create a new child hierarchy from this depth. 79 | if (current != null) 80 | { 81 | current.Child = new TestClass(height); 82 | } 83 | } 84 | 85 | public static Expression> GetValuePropertyExpression(int depth) 86 | { 87 | return _getValueExpression.GetOrAdd(depth, d => 88 | { 89 | if (_childPropertyInfo is null) 90 | { 91 | throw new InvalidOperationException(nameof(_childPropertyInfo)); 92 | } 93 | 94 | if (_valuePropertyInfo is null) 95 | { 96 | throw new InvalidOperationException(nameof(_valuePropertyInfo)); 97 | } 98 | 99 | ParameterExpression parameter = Expression.Parameter(typeof(TestClass), "x"); 100 | 101 | ParameterExpression pe = parameter; 102 | Expression body = pe; 103 | while (d-- > 1) 104 | { 105 | body = Expression.Property(body, _childPropertyInfo); 106 | } 107 | 108 | body = Expression.Property(body, _valuePropertyInfo); 109 | 110 | return Expression.Lambda>(body, parameter); 111 | }); 112 | } 113 | 114 | protected void RaiseAndSetIfChanged(ref T fieldValue, T value, [CallerMemberName] string propertyName = null) 115 | { 116 | if (EqualityComparer.Default.Equals(fieldValue, value)) 117 | { 118 | return; 119 | } 120 | 121 | fieldValue = value; 122 | OnPropertyChanged(propertyName); 123 | } 124 | 125 | protected virtual void OnPropertyChanged(string propertyName) 126 | { 127 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 128 | } 129 | 130 | /// 131 | object IViewFor.ViewModel 132 | { 133 | get => ViewModel; 134 | set => ViewModel = (TestClass)value; 135 | } 136 | 137 | /// 138 | public TestClass ViewModel { get; set; } 139 | } 140 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.Benchmarks/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using BenchmarkDotNet.Running; 6 | 7 | namespace ReactiveMarbles.PropertyChanged.Benchmarks 8 | { 9 | /// 10 | /// Class which hosts the main entry point into the application. 11 | /// 12 | public static class Program 13 | { 14 | /// 15 | /// The main entry point into the benchmarking application. 16 | /// 17 | /// Arguments from the command line. 18 | public static void Main(string[] args) 19 | { 20 | BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.Benchmarks/ReactiveMarbles.PropertyChanged.Benchmarks.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net6.0 5 | false 6 | true 7 | $(BaseIntermediateOutputPath)\GeneratedFiles 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | TextTemplatingFileGenerator 23 | BindBenchmarks.cs 24 | 25 | 26 | TextTemplatingFileGenerator 27 | WhenChangedBenchmarks.cs 28 | 29 | 30 | 31 | 32 | 33 | True 34 | True 35 | BindBenchmarks.tt 36 | 37 | 38 | True 39 | True 40 | WhenChangedBenchmarks.tt 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.Benchmarks/WhenChangedBenchmarks.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | 7 | using BenchmarkDotNet.Attributes; 8 | using BenchmarkDotNet.Configs; 9 | using BenchmarkDotNet.Jobs; 10 | 11 | using ReactiveMarbles.PropertyChanged.Benchmarks.Moqs; 12 | 13 | using SourceGen = NotifyPropertyExtensions; 14 | 15 | namespace ReactiveMarbles.PropertyChanged.Benchmarks 16 | { 17 | /// 18 | /// Benchmarks for the property changed. 19 | /// 20 | [SimpleJob(RuntimeMoniker.NetCoreApp31)] 21 | [MemoryDiagnoser] 22 | [MarkdownExporterAttribute.GitHub] 23 | [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] 24 | public class WhenChangedBenchmarks 25 | { 26 | private TestClass _from; 27 | private int _to; 28 | private IDisposable _subscription; 29 | 30 | /// 31 | /// The number mutations to perform. 32 | /// 33 | [Params(1, 10, 100, 1000)] 34 | public int Changes; 35 | 36 | [GlobalSetup(Targets = new[] { "SubscribeAndChange_Depth1_SourceGen" })] 37 | public void Depth1Setup() 38 | { 39 | _from = new TestClass(1); 40 | } 41 | 42 | [GlobalSetup(Targets = new[] { "SubscribeAndChange_Depth2_SourceGen" })] 43 | public void Depth2Setup() 44 | { 45 | _from = new TestClass(2); 46 | } 47 | 48 | [GlobalSetup(Targets = new[] { "SubscribeAndChange_Depth3_SourceGen" })] 49 | public void Depth3Setup() 50 | { 51 | _from = new TestClass(3); 52 | } 53 | 54 | public void PerformMutations(int depth) 55 | { 56 | // We loop through the changes, creating mutations at every depth. 57 | for (int i = 0; i < Changes; ++i) 58 | { 59 | _from.Mutate(i % depth); 60 | } 61 | } 62 | 63 | [BenchmarkCategory("Subscribe and Change Depth 1")] 64 | [Benchmark(Baseline = true)] 65 | public void SubscribeAndChange_Depth1_SourceGen() 66 | { 67 | using var subscription = SourceGen.WhenChanged(_from, x => x.Value).Subscribe(x => _to = x); 68 | PerformMutations(1); 69 | } 70 | 71 | [BenchmarkCategory("Subscribe and Change Depth 2")] 72 | [Benchmark(Baseline = true)] 73 | public void SubscribeAndChange_Depth2_SourceGen() 74 | { 75 | using var subscription = SourceGen.WhenChanged(_from, x => x.Child.Value).Subscribe(x => _to = x); 76 | PerformMutations(2); 77 | } 78 | 79 | [BenchmarkCategory("Subscribe and Change Depth 3")] 80 | [Benchmark(Baseline = true)] 81 | public void SubscribeAndChange_Depth3_SourceGen() 82 | { 83 | using var subscription = SourceGen.WhenChanged(_from, x => x.Child.Child.Value).Subscribe(x => _to = x); 84 | PerformMutations(3); 85 | } 86 | 87 | [GlobalSetup(Target = "Change_Depth1_SourceGen")] 88 | public void Change_Depth1_SourceGenSetup() 89 | { 90 | Depth1Setup(); 91 | _subscription = SourceGen.WhenChanged(_from, x => x.Value).Subscribe(x => _to = x); 92 | } 93 | 94 | [BenchmarkCategory("Change Depth 1")] 95 | [Benchmark(Baseline = true)] 96 | public void Change_Depth1_SourceGen() 97 | { 98 | PerformMutations(1); 99 | } 100 | 101 | [GlobalSetup(Target = "Change_Depth2_SourceGen")] 102 | public void Change_Depth2_SourceGenSetup() 103 | { 104 | Depth2Setup(); 105 | _subscription = SourceGen.WhenChanged(_from, x => x.Child.Value).Subscribe(x => _to = x); 106 | } 107 | 108 | [BenchmarkCategory("Change Depth 2")] 109 | [Benchmark(Baseline = true)] 110 | public void Change_Depth2_SourceGen() 111 | { 112 | PerformMutations(2); 113 | } 114 | 115 | [GlobalSetup(Target = "Change_Depth3_SourceGen")] 116 | public void Change_Depth3_SourceGenSetup() 117 | { 118 | Depth3Setup(); 119 | _subscription = SourceGen.WhenChanged(_from, x => x.Child.Child.Value).Subscribe(x => _to = x); 120 | } 121 | 122 | [BenchmarkCategory("Change Depth 3")] 123 | [Benchmark(Baseline = true)] 124 | public void Change_Depth3_SourceGen() 125 | { 126 | PerformMutations(3); 127 | } 128 | 129 | [GlobalCleanup(Targets = new[] { "Change_Depth1_SourceGen", "Change_Depth2_SourceGen", "Change_Depth3_SourceGen" })] 130 | public void GlobalCleanup() 131 | { 132 | _subscription.Dispose(); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.Benchmarks/WhenChangedBenchmarks.tt: -------------------------------------------------------------------------------- 1 | <#@ template debug="false" hostspecific="false" language="C#" #> 2 | <#@ assembly name="System.Core.dll" #> 3 | <#@ assembly name="System.Collections.dll" #> 4 | <#@ import namespace="System.Linq" #> 5 | <#@ output extension=".cs" #> 6 | // Copyright (c) 2019-2020 ReactiveUI Association Incorporated. All rights reserved. 7 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 8 | // See the LICENSE file in the project root for full license information. 9 | 10 | using System; 11 | using BenchmarkDotNet.Attributes; 12 | using BenchmarkDotNet.Configs; 13 | using BenchmarkDotNet.Jobs; 14 | 15 | using ReactiveMarbles.PropertyChanged.Benchmarks.Moqs; 16 | <# 17 | // First entry will be classified as the baseline. 18 | var participants = new (string Alias, string MethodName, string FullClassName)[] 19 | { 20 | ////("UI", "WhenAnyValue", "ReactiveUI.WhenAnyMixin"), 21 | ////("Old", "WhenChanged", "ReactiveMarbles.PropertyChanged.Benchmarks.Legacy.NotifyPropertyChangedExtensions"), 22 | ////("New", "WhenChanged", "ReactiveMarbles.PropertyChanged.NotifyPropertyChangedExtensions"), 23 | ("SourceGen", "WhenChanged", "NotifyPropertyChangedExtensions"), 24 | }; 25 | var depths = new[] { 1, 2, 3 }; 26 | const string SubscribeAndChange = "SubscribeAndChange"; 27 | const string Change = "Change"; 28 | string GetBenchmarkName(string baseName, int depth, string alias) => $"{baseName}_Depth{depth}_{alias}"; 29 | 30 | foreach(var (Alias, MethodName, FullClassName) in participants) 31 | { 32 | #> 33 | using <#= Alias #> = <#= FullClassName #>; 34 | <#}#> 35 | 36 | namespace ReactiveMarbles.PropertyChanged.Benchmarks 37 | { 38 | /// 39 | /// Benchmarks for the property changed. 40 | /// 41 | [SimpleJob(RuntimeMoniker.NetCoreApp31)] 42 | [MemoryDiagnoser] 43 | [MarkdownExporterAttribute.GitHub] 44 | [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] 45 | public class WhenChangedBenchmarks 46 | { 47 | private TestClass _from; 48 | private int _to; 49 | private IDisposable _subscription; 50 | 51 | /// 52 | /// The number mutations to perform. 53 | /// 54 | [Params(1, 10, 100, 1000)] 55 | public int Changes; 56 | 57 | <# 58 | for(var i = 0; i < depths.Length; i++) 59 | { 60 | var depth = depths[i]; #> 61 | [GlobalSetup(Targets = new[] { <#= string.Join(", ", participants.Select(x => $"\"{GetBenchmarkName(SubscribeAndChange, depth, x.Alias)}\"")) #> })] 62 | public void Depth<#= depth #>Setup() 63 | { 64 | _from = new TestClass(<#= depth #>); 65 | } 66 | 67 | <# } #> 68 | public void PerformMutations(int depth) 69 | { 70 | // We loop through the changes, creating mutations at every depth. 71 | for (var i = 0; i < Changes; ++i) 72 | { 73 | _from.Mutate(i % depth); 74 | } 75 | } 76 | 77 | <# 78 | for(var i = 0; i < depths.Length; i++) 79 | { 80 | var depth = depths[i]; 81 | for (var j = 0; j < participants.Length; j++) 82 | { 83 | var (Alias, MethodName, FullClassName) = participants[j]; #> 84 | [BenchmarkCategory("Subscribe and Change Depth <#= depth #>")] 85 | <# if (j == 0) WriteLine("[Benchmark(Baseline = true)]"); else WriteLine("[Benchmark]"); #> 86 | public void <#= GetBenchmarkName(SubscribeAndChange, depth, Alias) #>() 87 | { 88 | <# 89 | var expression = string.Join(".", Enumerable.Range(1, depth - 1).Select(x => "Child").Prepend("x").Append("Value")); #> 90 | using var subscription = <#= $"{Alias}.{MethodName}(_from, x => {expression}).Subscribe(x => _to = x);" #> 91 | PerformMutations(<#= depth #>); 92 | } 93 | 94 | <# } #> 95 | <# } #> 96 | <# 97 | for(var i = 0; i < depths.Length; i++) 98 | { 99 | var depth = depths[i]; 100 | for (var j = 0; j < participants.Length; j++) 101 | { 102 | var (Alias, MethodName, FullClassName) = participants[j]; 103 | var benchmarkName = GetBenchmarkName(Change, depth, Alias); #> 104 | [GlobalSetup(Target = <#= $"\"{benchmarkName}\"" #>)] 105 | public void <#= $"{benchmarkName}Setup" #>() 106 | { 107 | <# 108 | var expression = string.Join(".", Enumerable.Range(1, depth - 1).Select(x => "Child").Prepend("x").Append("Value")); #> 109 | Depth<#= depth #>Setup(); 110 | _subscription = <#= $"{Alias}.{MethodName}(_from, x => {expression}).Subscribe(x => _to = x);" #> 111 | } 112 | 113 | [BenchmarkCategory("Change Depth <#= depth #>")] 114 | <# if (j == 0) WriteLine("[Benchmark(Baseline = true)]"); else WriteLine("[Benchmark]"); #> 115 | public void <#= benchmarkName #>() 116 | { 117 | PerformMutations(<#= depth #>); 118 | } 119 | 120 | <# } #> 121 | <# 122 | } #> 123 | [GlobalCleanup(Targets = new[] { <#= string.Join(", ", depths.SelectMany(depth => participants.Select(x => $"\"{GetBenchmarkName(Change, depth, x.Alias)}\""))) #> })] 124 | public void GlobalCleanup() 125 | { 126 | _subscription.Dispose(); 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator.Benchmarks/.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | # override for benchmarks. 3 | 4 | # top-most EditorConfig file 5 | root = true -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator.Benchmarks/Directory.Build.props: -------------------------------------------------------------------------------- 1 |  2 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator.Benchmarks/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using BenchmarkDotNet.Running; 6 | 7 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.Benchmarks 8 | { 9 | /// 10 | /// Class which hosts the main execution point. 11 | /// 12 | public static class Program 13 | { 14 | /// 15 | /// The main entry point into the benchmarking application. 16 | /// 17 | /// Arguments from the command line. 18 | public static void Main(string[] args) 19 | { 20 | BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); 21 | ////var benchmark = new WhenChangedBenchmarks(); 22 | ////benchmark.InvocationKind = InvocationKind.MemberAccess; 23 | ////benchmark.Accessibility = Microsoft.CodeAnalysis.Accessibility.Public; 24 | ////benchmark.IsRoslyn = true; 25 | ////benchmark.Depth10WhenChangedSetup(); 26 | ////benchmark.Depth10WhenChanged(); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator.Benchmarks/ReactiveMarbles.PropertyChanged.SourceGenerator.Benchmarks.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | TextTemplatingFileGenerator 24 | WhenChangedBenchmarks.cs 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | True 35 | True 36 | WhenAnyBenchmarks.tt 37 | 38 | 39 | True 40 | True 41 | WhenChangedBenchmarks.tt 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator.Benchmarks/WhenChangedBenchmarks.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Threading.Tasks; 6 | 7 | using BenchmarkDotNet.Attributes; 8 | using BenchmarkDotNet.Configs; 9 | using BenchmarkDotNet.Jobs; 10 | 11 | using Microsoft.CodeAnalysis; 12 | 13 | using ReactiveMarbles.PropertyChanged.SourceGenerator.Builders; 14 | 15 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.Benchmarks 16 | { 17 | [SimpleJob(RuntimeMoniker.NetCoreApp31)] 18 | [MemoryDiagnoser] 19 | [MarkdownExporterAttribute.GitHub] 20 | [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] 21 | public class WhenChangedBenchmarks 22 | { 23 | public CompilationUtil Compilation { get; set; } 24 | 25 | [ParamsAllValues] 26 | public InvocationKind InvocationKind { get; set; } 27 | 28 | [ParamsAllValues] 29 | public ReceiverKind ReceiverKind { get; set; } 30 | 31 | [Params(Accessibility.Public, Accessibility.Private)] 32 | public Accessibility Accessibility { get; set; } 33 | 34 | public string UserSource { get; set; } 35 | 36 | 37 | [GlobalSetup(Targets = new[] { nameof(Depth1WhenChanged) })] 38 | public Task Depth1WhenChangedSetup() 39 | { 40 | EmptyClassBuilder hostPropertyTypeInfo = new EmptyClassBuilder() 41 | .WithClassAccess(Accessibility); 42 | UserSource = new WhenChangedHostBuilder() 43 | .WithClassAccess(Accessibility) 44 | .WithPropertyType(hostPropertyTypeInfo) 45 | .WithInvocation(InvocationKind, x => x.Value) 46 | .BuildSource(); 47 | 48 | Compilation = new CompilationUtil(_ => { }); 49 | return Compilation.Initialize(); 50 | } 51 | 52 | [Benchmark] 53 | [BenchmarkCategory("Change Depth 1")] 54 | public void Depth1WhenChanged() 55 | { 56 | var generator = Compilation.RunGenerators(out _, out _, out _, out _, new[] { ("usersource.cs", UserSource) }); 57 | } 58 | [GlobalSetup(Targets = new[] { nameof(Depth2WhenChanged) })] 59 | public Task Depth2WhenChangedSetup() 60 | { 61 | EmptyClassBuilder hostPropertyTypeInfo = new EmptyClassBuilder() 62 | .WithClassAccess(Accessibility); 63 | UserSource = new WhenChangedHostBuilder() 64 | .WithClassAccess(Accessibility) 65 | .WithPropertyType(hostPropertyTypeInfo) 66 | .WithInvocation(InvocationKind, x => x.Child.Value) 67 | .BuildSource(); 68 | 69 | Compilation = new CompilationUtil(_ => { }); 70 | return Compilation.Initialize(); 71 | } 72 | 73 | [Benchmark] 74 | [BenchmarkCategory("Change Depth 2")] 75 | public void Depth2WhenChanged() 76 | { 77 | var generator = Compilation.RunGenerators(out _, out _, out _, out _, new[] { ("usersource.cs", UserSource) }); 78 | } 79 | [GlobalSetup(Targets = new[] { nameof(Depth10WhenChanged) })] 80 | public Task Depth10WhenChangedSetup() 81 | { 82 | EmptyClassBuilder hostPropertyTypeInfo = new EmptyClassBuilder() 83 | .WithClassAccess(Accessibility); 84 | UserSource = new WhenChangedHostBuilder() 85 | .WithClassAccess(Accessibility) 86 | .WithPropertyType(hostPropertyTypeInfo) 87 | .WithInvocation(InvocationKind, x => x.Child.Child.Child.Child.Child.Child.Child.Child.Child.Value) 88 | .BuildSource(); 89 | 90 | Compilation = new CompilationUtil(_ => { }); 91 | return Compilation.Initialize(); 92 | } 93 | 94 | [Benchmark] 95 | [BenchmarkCategory("Change Depth 10")] 96 | public void Depth10WhenChanged() 97 | { 98 | var generator = Compilation.RunGenerators(out _, out _, out _, out _, new[] { ("usersource.cs", UserSource) }); 99 | } 100 | 101 | [GlobalSetup(Targets = new[] { nameof(Depth20WhenChanged) })] 102 | public Task Depth20WhenChangedSetup() 103 | { 104 | EmptyClassBuilder hostPropertyTypeInfo = new EmptyClassBuilder() 105 | .WithClassAccess(Accessibility); 106 | UserSource = new WhenChangedHostBuilder() 107 | .WithClassAccess(Accessibility) 108 | .WithPropertyType(hostPropertyTypeInfo) 109 | .WithInvocation(InvocationKind, x => x.Child.Child.Child.Child.Child.Child.Child.Child.Child.Child.Child.Child.Child.Child.Child.Child.Child.Child.Child.Value) 110 | .BuildSource(); 111 | 112 | Compilation = new CompilationUtil(_ => { }); 113 | return Compilation.Initialize(); 114 | } 115 | 116 | [Benchmark] 117 | [BenchmarkCategory("Change Depth 20")] 118 | public void Depth20WhenChanged() 119 | { 120 | var generator = Compilation.RunGenerators(out _, out _, out _, out _, new[] { ("usersource.cs", UserSource) }); 121 | } 122 | 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator.Benchmarks/WhenChangedBenchmarks.tt: -------------------------------------------------------------------------------- 1 | <#@ template debug="false" hostspecific="false" language="C#" #> 2 | <#@ assembly name="System.Core.dll" #> 3 | <#@ assembly name="System.Collections.dll" #> 4 | <#@ import namespace="System.Linq" #> 5 | <#@ output extension=".cs" #> 6 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 7 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 8 | // See the LICENSE file in the project root for full license information. 9 | 10 | using System; 11 | 12 | using BenchmarkDotNet.Attributes; 13 | using BenchmarkDotNet.Configs; 14 | using BenchmarkDotNet.Jobs; 15 | 16 | using Microsoft.CodeAnalysis; 17 | 18 | using ReactiveMarbles.PropertyChanged.SourceGenerator.Builders; 19 | 20 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.Benchmarks 21 | { 22 | [SimpleJob(RuntimeMoniker.NetCoreApp31)] 23 | [MemoryDiagnoser] 24 | [MarkdownExporterAttribute.GitHub] 25 | [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] 26 | public class WhenChangedBenchmarks 27 | { 28 | public Compilation Compilation { get; set; } 29 | 30 | [ParamsAllValues] 31 | public InvocationKind InvocationKind { get; set; } 32 | 33 | [ParamsAllValues] 34 | public ReceiverKind ReceiverKind { get; set; } 35 | 36 | [Params(Accessibility.Public, Accessibility.Private)] 37 | public Accessibility Accessibility { get; set; } 38 | 39 | <# 40 | var depths = new[] { 1, 2, 10, 20 }; 41 | #> 42 | 43 | <# 44 | for (var i = 0; i < depths.Length; i++) 45 | { 46 | var depth = depths[i]; 47 | var expression = string.Join(".", Enumerable.Range(1, depth - 1).Select(x => "Child").Prepend("x").Append("Value")); 48 | #> 49 | [GlobalSetup(Targets = new[] { nameof(Depth<#= depth #>WhenChanged) })] 50 | public void Depth<#= depth #>WhenChangedSetup() 51 | { 52 | var hostPropertyTypeInfo = new EmptyClassBuilder() 53 | .WithClassAccess(Accessibility); 54 | string userSource = new WhenChangedHostBuilder() 55 | .WithClassAccess(Accessibility) 56 | .WithPropertyType(hostPropertyTypeInfo) 57 | .WithInvocation(InvocationKind, ReceiverKind, x => <#= expression #>) 58 | .BuildSource(); 59 | 60 | Compilation = CompilationUtil.CreateCompilation(userSource); 61 | } 62 | 63 | [Benchmark] 64 | [BenchmarkCategory("Change Depth <#= depth #>")] 65 | public void Depth<#= depth #>WhenChanged() 66 | { 67 | var newCompilation = CompilationUtil.RunGenerators(Compilation, out _, new Generator()); 68 | } 69 | <# 70 | } 71 | #> 72 | 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator.Builders/AccessibilityExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using Microsoft.CodeAnalysis; 6 | 7 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.Builders; 8 | 9 | internal static class AccessibilityExtensions 10 | { 11 | public static string ToFriendlyString(this Accessibility accessibility) => 12 | accessibility switch 13 | { 14 | Accessibility.Public => "public", 15 | Accessibility.Internal => "internal", 16 | Accessibility.Private => "private", 17 | Accessibility.NotApplicable => string.Empty, 18 | Accessibility.ProtectedAndInternal => "private protected", 19 | Accessibility.Protected => "protected", 20 | Accessibility.ProtectedOrInternal => "protected internal", 21 | _ => string.Empty, 22 | }; 23 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator.Builders/BindHostProxy.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | 7 | using ReactiveMarbles.PropertyChanged.SourceGenerator.Builders; 8 | 9 | /// 10 | /// Proxies a Bind host. 11 | /// 12 | public class BindHostProxy : HostProxyBase 13 | { 14 | private WhenChangedHostProxy? _viewModelProxy; 15 | 16 | /// 17 | /// Initializes a new instance of the class. 18 | /// 19 | /// The source to proxy. 20 | public BindHostProxy(object source) 21 | : base(source) 22 | { 23 | } 24 | 25 | /// 26 | /// Gets or sets the view model value. 27 | /// 28 | public WhenChangedHostProxy? ViewModel 29 | { 30 | get => _viewModelProxy; 31 | set 32 | { 33 | _viewModelProxy = value; 34 | ReflectionUtil.SetProperty(Source, nameof(ViewModel), value?.Source); 35 | } 36 | } 37 | 38 | /// 39 | /// Gets or sets the view value. 40 | /// 41 | public object? Value 42 | { 43 | get => ReflectionUtil.GetProperty(Source, nameof(Value)); 44 | set => ReflectionUtil.SetProperty(Source, nameof(Value), value); 45 | } 46 | 47 | /// 48 | /// Gets the observable resulting from the WhenChanged view model invocation. 49 | /// 50 | /// An action to invoke when the WhenChanged implementation doesn't generate correctly. 51 | /// An observable. 52 | public IObservable GetViewModelWhenChangedObservable(Action onError) 53 | { 54 | try 55 | { 56 | var methodInstance = GetMethod(Source, MethodNames.GetWhenChangedObservable) ?? throw new InvalidOperationException("Must have valid method"); 57 | return (IObservable)methodInstance; 58 | } 59 | catch (Exception ex) 60 | { 61 | onError?.Invoke(ex); 62 | throw; 63 | } 64 | } 65 | 66 | /// 67 | /// Gets the observable resulting from the WhenChanged view invocation. 68 | /// 69 | /// An action to invoke when the WhenChanged implementation doesn't generate correctly. 70 | /// An observable. 71 | public IObservable GetWhenChangedObservable(Action onError) 72 | { 73 | try 74 | { 75 | var methodInstance = GetMethod(Source, MethodNames.GetWhenChangedObservable) ?? throw new InvalidOperationException("Must have valid method"); 76 | 77 | return (IObservable)methodInstance; 78 | } 79 | catch (Exception ex) 80 | { 81 | onError?.Invoke(ex); 82 | throw; 83 | } 84 | } 85 | 86 | /// 87 | /// Gets the observable resulting from the OneWayBind invocation. 88 | /// 89 | /// An action to invoke when the OneWayBind implementation doesn't generate correctly. 90 | /// An observable. 91 | public IDisposable GetOneWayBindSubscription(Action onError) 92 | { 93 | try 94 | { 95 | var methodInstance = GetMethod(Source, MethodNames.GetBindOneWaySubscription) ?? throw new InvalidOperationException("Must have valid method"); 96 | 97 | return (IDisposable)methodInstance; 98 | } 99 | catch (Exception ex) 100 | { 101 | onError?.Invoke(ex); 102 | throw; 103 | } 104 | } 105 | 106 | /// 107 | /// Gets the observable resulting from the Bind invocation. 108 | /// 109 | /// An action to invoke when the Bind implementation doesn't generate correctly. 110 | /// An observable. 111 | public IDisposable GetTwoWayBindSubscription(Action onError) 112 | { 113 | try 114 | { 115 | var methodInstance = GetMethod(Source, MethodNames.GetBindTwoWaySubscription) ?? throw new InvalidOperationException("Must have valid method"); 116 | 117 | return (IDisposable)methodInstance; 118 | } 119 | catch (Exception ex) 120 | { 121 | onError?.Invoke(ex); 122 | throw; 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator.Builders/CompilationUtil.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Collections.Immutable; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Reflection; 10 | using System.Threading.Tasks; 11 | 12 | using ICSharpCode.Decompiler.Metadata; 13 | 14 | using Microsoft.CodeAnalysis; 15 | using Microsoft.CodeAnalysis.CSharp; 16 | 17 | using NuGet.LibraryModel; 18 | using NuGet.Versioning; 19 | 20 | using ReactiveMarbles.NuGet.Helpers; 21 | using ReactiveMarbles.SourceGenerator.TestNuGetHelper.Compilation; 22 | 23 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.Builders; 24 | 25 | /// 26 | /// Utility methods to assist with compilations. 27 | /// 28 | public sealed class CompilationUtil : IDisposable 29 | { 30 | #pragma warning disable CS0618 // Type or member is obsolete 31 | private readonly LibraryRange _reactiveLibrary = new("System.Reactive", VersionRange.AllStableFloating, LibraryDependencyTarget.Package); 32 | #pragma warning restore CS0618 // Type or member is obsolete 33 | 34 | private readonly SourceGeneratorUtility _sourceGeneratorUtility; 35 | 36 | /// 37 | /// Initializes a new instance of the class. 38 | /// 39 | /// Outputs any warning text. 40 | public CompilationUtil(Action outputHelper) => _sourceGeneratorUtility = new SourceGeneratorUtility(outputHelper); 41 | 42 | private EventBuilderCompiler? EventCompiler { get; set; } 43 | 44 | /// 45 | /// Initializes. 46 | /// 47 | /// Task to monitor the progress. 48 | public async Task Initialize() 49 | { 50 | var targetFrameworks = "netstandard2.0".ToFrameworks(); 51 | 52 | var inputGroup = await NuGetPackageHelper.DownloadPackageFilesAndFolder(_reactiveLibrary, targetFrameworks, packageOutputDirectory: null).ConfigureAwait(false); 53 | 54 | var framework = targetFrameworks[0]; 55 | EventCompiler = new(inputGroup, inputGroup, framework); 56 | } 57 | 58 | /// 59 | /// Executes the source generators. 60 | /// 61 | /// The resulting diagnostics from compilation. 62 | /// The resulting diagnostics from generation. 63 | /// The compiler before generation. 64 | /// The compiler after generation. 65 | /// The source code. 66 | /// The generator. 67 | public GeneratorDriver RunGenerators(out ImmutableArray compilationDiagnostics, out ImmutableArray generatorDiagnostics, out Compilation beforeGeneratorCompilation, out Compilation afterGeneratorCompilation, params (string FileName, string Source)[] sources) 68 | { 69 | if (EventCompiler is null) 70 | { 71 | throw new InvalidOperationException("Must have valid compiler instance."); 72 | } 73 | 74 | _sourceGeneratorUtility.RunGenerator(EventCompiler, out compilationDiagnostics, out generatorDiagnostics, out var driver, out beforeGeneratorCompilation, out afterGeneratorCompilation, sources); 75 | return driver; 76 | } 77 | 78 | /// 79 | public void Dispose() => EventCompiler?.Dispose(); 80 | } 81 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator.Builders/EmptyClassBuilder.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.Builders; 6 | 7 | /// 8 | /// Simplifies the source code creation of an empty class. 9 | /// 10 | public class EmptyClassBuilder : BaseUserSourceBuilder 11 | { 12 | /// 13 | protected override string CreateClass(string nestedClasses) => 14 | $@" 15 | {ClassAccess.ToFriendlyString()} partial class {ClassName} 16 | {{ 17 | {nestedClasses} 18 | }} 19 | "; 20 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator.Builders/HostProxyBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Reflection; 7 | 8 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.Builders; 9 | 10 | /// 11 | /// A base class for the host proxy which allows interaction with a roslyn compiled class. 12 | /// 13 | public abstract class HostProxyBase 14 | { 15 | private WhenChangedHostProxy? _child; 16 | 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | /// An instance of the actual host class. 21 | protected HostProxyBase(object source) => Source = source ?? throw new ArgumentNullException(nameof(source)); 22 | 23 | /// 24 | /// Gets the actual host object. 25 | /// 26 | public object Source { get; } 27 | 28 | /// 29 | /// Gets or sets the child. 30 | /// 31 | public WhenChangedHostProxy? Child 32 | { 33 | get => _child; 34 | 35 | set 36 | { 37 | _child = value; 38 | ReflectionUtil.SetProperty(Source, nameof(Child), value?.Source); 39 | } 40 | } 41 | 42 | internal static object? GetMethod(object target, string methodName) => 43 | target.GetType().InvokeMember( 44 | methodName, 45 | BindingFlags.InvokeMethod | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, 46 | null, 47 | target, 48 | Array.Empty()); 49 | } 50 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator.Builders/InvocationKind.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.Builders; 6 | 7 | /// 8 | /// The type of invocation. 9 | /// 10 | public enum InvocationKind 11 | { 12 | /// 13 | /// The invocation is a member access. 14 | /// 15 | MemberAccess, 16 | 17 | /// 18 | /// The invocation is explicit. 19 | /// 20 | Explicit, 21 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator.Builders/MethodNames.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.Builders; 6 | 7 | internal static class MethodNames 8 | { 9 | public const string GetWhenChangingObservable = "GetWhenChangingObservable"; 10 | public const string GetWhenChangedObservable = "GetWhenChangedObservable"; 11 | public const string GetBindOneWaySubscription = "GetBindOneWaySubscription"; 12 | public const string GetBindTwoWaySubscription = "GetBindTwoWaySubscription"; 13 | public const string GetWhenChangedHostObservable = "GetWhenChangedHostObservable"; 14 | public const string GetWhenChangedTargetObservable = "GetWhenChangedTargetObservable"; 15 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator.Builders/ReactiveMarbles.PropertyChanged.SourceGenerator.Builders.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | preview 6 | false 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator.Builders/ReceiverKind.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.Builders; 6 | 7 | /// 8 | /// The type of receiver. 9 | /// 10 | public enum ReceiverKind 11 | { 12 | /// 13 | /// The receiver is a 'this' reference. 14 | /// 15 | This, 16 | 17 | /// 18 | /// The receiver is a instance reference. 19 | /// 20 | Instance, 21 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator.Builders/ReflectionUtil.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Reflection; 7 | 8 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.Builders; 9 | 10 | /// 11 | /// Utility for common reflection operations. 12 | /// 13 | public static class ReflectionUtil 14 | { 15 | /// 16 | /// Sets a property on a target object. 17 | /// 18 | /// The object to invoke the setter on. 19 | /// The name of the property. 20 | /// The value to assign. 21 | public static void SetProperty(object target, string propertyName, object? value) 22 | { 23 | if (target is null) 24 | { 25 | throw new ArgumentNullException(nameof(target)); 26 | } 27 | 28 | if (string.IsNullOrWhiteSpace(propertyName)) 29 | { 30 | throw new ArgumentException($"'{nameof(propertyName)}' cannot be null or whitespace.", nameof(propertyName)); 31 | } 32 | 33 | target.GetType().GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).SetValue(target, value); 34 | } 35 | 36 | /// 37 | /// Gets a property on a target object. 38 | /// 39 | /// The object to invoke the getter on. 40 | /// The name of the property. 41 | /// The value of the property. 42 | public static object? GetProperty(object target, string propertyName) 43 | { 44 | if (target is null) 45 | { 46 | throw new ArgumentNullException(nameof(target)); 47 | } 48 | 49 | if (string.IsNullOrWhiteSpace(propertyName)) 50 | { 51 | throw new ArgumentException($"'{nameof(propertyName)}' cannot be null or whitespace.", nameof(propertyName)); 52 | } 53 | 54 | return target.GetType().GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).GetValue(target); 55 | } 56 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator.Builders/WhenChangedHostProxy.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | 7 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.Builders; 8 | 9 | /// 10 | /// Acts as a user-friendly interface to interact with the 'host' type in the generated compilation. 11 | /// 12 | public class WhenChangedHostProxy : HostProxyBase 13 | { 14 | private WhenChangedHostProxy? _receiver; 15 | 16 | /// 17 | /// Initializes a new instance of the class. 18 | /// 19 | /// The source object to proxy. 20 | public WhenChangedHostProxy(object source) 21 | : base(source) 22 | { 23 | } 24 | 25 | /// 26 | /// Gets or sets the value. 27 | /// 28 | public object? Value 29 | { 30 | get => ReflectionUtil.GetProperty(Source, nameof(Value)); 31 | set => ReflectionUtil.SetProperty(Source, nameof(Value), value); 32 | } 33 | 34 | /// 35 | /// Gets or sets the receiver. 36 | /// 37 | public WhenChangedHostProxy? Receiver 38 | { 39 | get => _receiver; 40 | 41 | set 42 | { 43 | _receiver = value; 44 | ReflectionUtil.SetProperty(Source, nameof(Receiver), value?.Source); 45 | } 46 | } 47 | 48 | /// 49 | /// Gets the observable resulting from the WhenChanging invocation. 50 | /// 51 | /// An action to invoke when the WhenChanging implementation doesn't generate correctly. 52 | /// An observable. 53 | public IObservable? GetWhenChangingObservable(Action onError) 54 | { 55 | try 56 | { 57 | return GetMethod(Source, MethodNames.GetWhenChangingObservable) as IObservable; 58 | } 59 | catch (Exception ex) 60 | { 61 | onError?.Invoke(ex); 62 | throw; 63 | } 64 | } 65 | 66 | /// 67 | /// Gets the observable resulting from the WhenChanged invocation. 68 | /// 69 | /// An action to invoke when the WhenChanged implementation doesn't generate correctly. 70 | /// An observable. 71 | public IObservable? GetWhenChangedObservable(Action onError) 72 | { 73 | try 74 | { 75 | return GetMethod(Source, MethodNames.GetWhenChangedObservable) as IObservable; 76 | } 77 | catch (Exception ex) 78 | { 79 | onError?.Invoke(ex); 80 | throw; 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator.Sample/OtherNamespace/SampleClass.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.ComponentModel; 6 | 7 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.Sample.OtherNamespace 8 | { 9 | /// 10 | /// Dummy. 11 | /// 12 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Readability", "RCS1018:Add accessibility modifiers.", Justification = "Because")] 13 | [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1400:Access modifier should be declared", Justification = "Because")] 14 | public class SampleClass : INotifyPropertyChanged, INotifyPropertyChanging 15 | { 16 | private Sample.SampleClass _myClass; 17 | private string _myString; 18 | private string _myString2; 19 | private string _myString3; 20 | 21 | internal SampleClass() 22 | { 23 | } 24 | 25 | /// 26 | /// Dummy. 27 | /// 28 | public event PropertyChangedEventHandler PropertyChanged; 29 | 30 | /// 31 | /// Dummy. 32 | /// 33 | public event PropertyChangingEventHandler PropertyChanging; 34 | 35 | /// 36 | /// Gets or sets a string. 37 | /// 38 | internal string MyString 39 | { 40 | get => _myString; 41 | 42 | set 43 | { 44 | PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(nameof(MyString))); 45 | _myString = value; 46 | PropertyChanged?.Invoke(this, new(nameof(MyString))); 47 | } 48 | } 49 | 50 | /// 51 | /// Gets or sets a string. 52 | /// 53 | internal string MyString2 54 | { 55 | get => _myString2; 56 | 57 | set 58 | { 59 | _myString2 = value; 60 | PropertyChanged?.Invoke(this, new(nameof(MyString2))); 61 | } 62 | } 63 | 64 | /// 65 | /// Gets or sets a string. 66 | /// 67 | internal string MyString3 68 | { 69 | get => _myString3; 70 | 71 | set 72 | { 73 | _myString3 = value; 74 | PropertyChanged?.Invoke(this, new(nameof(MyString3))); 75 | } 76 | } 77 | 78 | /// 79 | /// Gets or sets a class. 80 | /// 81 | internal Sample.SampleClass MyClass 82 | { 83 | get => _myClass; 84 | 85 | set 86 | { 87 | PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(nameof(MyClass))); 88 | _myClass = value; 89 | PropertyChanged?.Invoke(this, new(nameof(MyClass))); 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator.Sample/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Linq.Expressions; 7 | using System.Reactive.Linq; 8 | 9 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.Sample 10 | { 11 | internal static class Program 12 | { 13 | public static void Main(string[] args) 14 | { 15 | var myClass = new SampleClass(); 16 | var myClass2 = new OtherNamespace.SampleClass(); 17 | var myClass3 = new SampleClass2(); 18 | 19 | myClass.WhenChanged(x => x.MyString).Where(x => x == "Hello World").Subscribe(Console.WriteLine); 20 | myClass.WhenChanged(x => x.MyString2).Where(x => x == "Hello World").Subscribe(Console.WriteLine); 21 | myClass.WhenChanged(x => x.MyString3).Where(x => x == "Hello World").Subscribe(Console.WriteLine); 22 | myClass2.WhenChanging(x => x.MyString).Subscribe(Console.WriteLine); 23 | myClass2.WhenChanging(x => x.MyString2).Subscribe(Console.WriteLine); 24 | myClass2.WhenChanging(x => x.MyString3).Subscribe(Console.WriteLine); 25 | myClass2.WhenChanging(x => x.MyString).Subscribe(Console.WriteLine); 26 | 27 | myClass.BindTwoWay(myClass3, x => x.MyString, x => x.MyString); 28 | myClass.BindTwoWay(myClass3, x => x.MyString, x => x.MyString); 29 | myClass.BindTwoWay(myClass3, x => x.MyString2, x => x.MyString2); 30 | myClass.BindTwoWay(myClass3, x => x.MyString3, x => x.MyString3); 31 | 32 | myClass2.BindOneWay(myClass2, x => x.MyString, x => x.MyString); 33 | myClass2.BindOneWay(myClass2, x => x.MyString2, x => x.MyString2); 34 | myClass2.BindOneWay(myClass2, x => x.MyString3, x => x.MyString3); 35 | 36 | Observable 37 | .Interval(TimeSpan.FromSeconds(1)) 38 | .Take(5) 39 | .Subscribe(x => myClass.MyString = x.ToString()); 40 | 41 | Console.ReadLine(); 42 | } 43 | 44 | private static Expression> GetExpression() => x => x.MyString; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator.Sample/ReactiveMarbles.PropertyChanged.SourceGenerator.Sample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | preview 7 | false 8 | true 9 | $(BaseIntermediateOutputPath)\GeneratedFiles 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator.Sample/SampleClass.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.ComponentModel; 7 | using System.Diagnostics.CodeAnalysis; 8 | using System.Reactive.Linq; 9 | 10 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.Sample 11 | { 12 | /// 13 | /// Dummy. 14 | /// 15 | public partial class SampleClass 16 | { 17 | private PrivateClass GetClass() => throw new NotImplementedException(); 18 | } 19 | 20 | /// 21 | /// Dummy. 22 | /// 23 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Readability", "RCS1018:Add accessibility modifiers.", Justification = "Because")] 24 | [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1400:Access modifier should be declared", Justification = "Because")] 25 | public partial class SampleClass : INotifyPropertyChanged, INotifyPropertyChanging 26 | { 27 | private OtherNamespace.SampleClass _myClass; 28 | private string _myString; 29 | private string _myString2; 30 | private string _myString3; 31 | 32 | internal SampleClass() => 33 | #pragma warning disable SX1101 // Do not prefix local calls with 'this.' 34 | this.WhenChanged(x => x.MyClass); 35 | #pragma warning restore SX1101 // Do not prefix local calls with 'this.' 36 | 37 | /// 38 | /// Dummy. 39 | /// 40 | public event PropertyChangedEventHandler PropertyChanged; 41 | 42 | /// 43 | /// Dummy. 44 | /// 45 | public event PropertyChangingEventHandler PropertyChanging; 46 | 47 | /// 48 | /// Gets or sets a string. 49 | /// 50 | internal string MyString 51 | { 52 | get => _myString; 53 | 54 | set 55 | { 56 | PropertyChanging?.Invoke(this, new(nameof(MyString))); 57 | _myString = value; 58 | PropertyChanged?.Invoke(this, new(nameof(MyString))); 59 | } 60 | } 61 | 62 | /// 63 | /// Gets or sets a string. 64 | /// 65 | internal string MyString2 66 | { 67 | get => _myString2; 68 | 69 | set 70 | { 71 | PropertyChanging?.Invoke(this, new(nameof(MyString2))); 72 | _myString2 = value; 73 | PropertyChanged?.Invoke(this, new(nameof(MyString2))); 74 | } 75 | } 76 | 77 | /// 78 | /// Gets or sets a string. 79 | /// 80 | internal string MyString3 81 | { 82 | get => _myString3; 83 | 84 | set 85 | { 86 | PropertyChanging?.Invoke(this, new(nameof(MyString3))); 87 | _myString3 = value; 88 | PropertyChanged?.Invoke(this, new(nameof(MyString3))); 89 | } 90 | } 91 | 92 | /// 93 | /// Gets or sets a class. 94 | /// 95 | internal OtherNamespace.SampleClass MyClass 96 | { 97 | get => _myClass; 98 | 99 | set 100 | { 101 | _myClass = value; 102 | PropertyChanged?.Invoke(this, new(nameof(MyClass))); 103 | } 104 | } 105 | 106 | [SuppressMessage("Design", "CA1812: Never used class", Justification = "Used by Rx")] 107 | private class PrivateClass 108 | { 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator.Sample/SampleClass2.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.ComponentModel; 6 | 7 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.Sample 8 | { 9 | /// 10 | /// Second sample class. 11 | /// 12 | public class SampleClass2 : INotifyPropertyChanged 13 | { 14 | private OtherNamespace.SampleClass _myClass; 15 | private string _myString; 16 | private string _myString2; 17 | private string _myString3; 18 | 19 | internal SampleClass2() => 20 | #pragma warning disable SX1101 // Do not prefix local calls with 'this.' 21 | this.WhenChanged(x => x.MyClass); 22 | #pragma warning restore SX1101 // Do not prefix local calls with 'this.' 23 | 24 | /// 25 | /// Dummy. 26 | /// 27 | public event PropertyChangedEventHandler PropertyChanged; 28 | 29 | /// 30 | /// Gets or sets a string. 31 | /// 32 | internal string MyString 33 | { 34 | get => _myString; 35 | 36 | set 37 | { 38 | _myString = value; 39 | PropertyChanged?.Invoke(this, new(nameof(MyString))); 40 | } 41 | } 42 | 43 | /// 44 | /// Gets or sets a string. 45 | /// 46 | internal string MyString2 47 | { 48 | get => _myString2; 49 | 50 | set 51 | { 52 | _myString2 = value; 53 | PropertyChanged?.Invoke(this, new(nameof(MyString2))); 54 | } 55 | } 56 | 57 | /// 58 | /// Gets or sets a string. 59 | /// 60 | internal string MyString3 61 | { 62 | get => _myString3; 63 | 64 | set 65 | { 66 | _myString3 = value; 67 | PropertyChanged?.Invoke(this, new(nameof(MyString3))); 68 | } 69 | } 70 | 71 | /// 72 | /// Gets or sets a class. 73 | /// 74 | internal OtherNamespace.SampleClass MyClass 75 | { 76 | get => _myClass; 77 | 78 | set 79 | { 80 | _myClass = value; 81 | PropertyChanged?.Invoke(this, new(nameof(MyClass))); 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator.Tests/AccessibilityTestCases.tt: -------------------------------------------------------------------------------- 1 | <#@ template debug="false" hostspecific="true" language="C#" #> 2 | <#@ assembly name="System.Core.dll" #> 3 | <#@ assembly name="System.Collections.dll" #> 4 | <#@ assembly name="System.Collections.Immutable.dll" #> 5 | <#@ assembly name="System.Runtime.dll" #> 6 | <#@ assembly name="Microsoft.CodeAnalysis.dll" #> 7 | <#@ assembly name="Microsoft.CodeAnalysis.CSharp.dll" #> 8 | <#@ assembly name="netstandard.dll" #> 9 | <#@ import namespace="System.IO" #> 10 | <#@ import namespace="System.Linq" #> 11 | <#@ output extension=".cs" #> 12 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 13 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 14 | // See the LICENSE file in the project root for full license information. 15 | 16 | <# 17 | string ToFriendlyString(Accessibility accessibility) 18 | { 19 | if (accessibility == Accessibility.Public) return "public"; 20 | if (accessibility == Accessibility.Internal) return "internal"; 21 | if (accessibility == Accessibility.Private) return "private"; 22 | if (accessibility == Accessibility.Protected) return "protected"; 23 | if (accessibility == Accessibility.ProtectedAndInternal) return "private protected"; 24 | if (accessibility == Accessibility.ProtectedOrInternal) return "protected internal"; 25 | 26 | return string.Empty; 27 | } 28 | 29 | bool ValidateAccessModifierCombination( 30 | Accessibility hostContainerTypeAccess, 31 | Accessibility hostTypeAccess, 32 | Accessibility propertyTypeAccess, 33 | Accessibility propertyAccess) 34 | { 35 | var hostContainerSource = $@" 36 | {ToFriendlyString(hostContainerTypeAccess)} class Container 37 | {{ 38 | {ToFriendlyString(hostTypeAccess)} class Host 39 | {{ 40 | {ToFriendlyString(propertyAccess)} CustomType Value {{ get; set; }} 41 | 42 | {ToFriendlyString(propertyTypeAccess)} class CustomType 43 | {{ 44 | }} 45 | }} 46 | }} 47 | "; 48 | 49 | Compilation compilation = CreateCompilation(hostContainerSource); 50 | return compilation.GetDiagnostics().All(x => x.Severity < DiagnosticSeverity.Error); 51 | } 52 | 53 | Compilation CreateCompilation(params string[] sources) 54 | { 55 | var assemblyPath = Path.GetDirectoryName(typeof(object).Assembly.Location); 56 | 57 | return CSharpCompilation.Create( 58 | assemblyName: "compilation", 59 | syntaxTrees: sources.Select(x => CSharpSyntaxTree.ParseText(x, new CSharpParseOptions(LanguageVersion.Latest))), 60 | references: new[] 61 | { 62 | MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "mscorlib.dll")), 63 | MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.dll")), 64 | MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Core.dll")), 65 | MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "System.Runtime.dll")), 66 | MetadataReference.CreateFromFile(Path.Combine(assemblyPath, "netstandard.dll")), 67 | }, 68 | options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); 69 | } 70 | 71 | Compilation RunGenerators(Compilation compilation, out ImmutableArray diagnostics, params ISourceGenerator[] generators) 72 | { 73 | CreateDriver(compilation, generators).RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out diagnostics); 74 | return outputCompilation; 75 | } 76 | 77 | GeneratorDriver CreateDriver(Compilation compilation, params ISourceGenerator[] generators) => 78 | CSharpGeneratorDriver.Create( 79 | generators: ImmutableArray.Create(generators), 80 | additionalTexts: ImmutableArray.Empty, 81 | parseOptions: (CSharpParseOptions)compilation.SyntaxTrees.First().Options, 82 | optionsProvider: null); 83 | #> 84 | using System.Collections.Generic; 85 | using Microsoft.CodeAnalysis; 86 | 87 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.Tests 88 | { 89 | internal static class AccessibilityTestCases 90 | { 91 | public static IEnumerable GetValidAccessModifierCombinations() 92 | { 93 | <# 94 | var hostContainerTypeAccessList = new[] { Accessibility.Public, Accessibility.Internal }; 95 | var hostTypeAccessList = new[] { Accessibility.Private, Accessibility.ProtectedAndInternal, Accessibility.Protected, Accessibility.Internal, Accessibility.ProtectedOrInternal, Accessibility.Public }; 96 | var propertyTypeAccessList = new[] { Accessibility.Private, Accessibility.ProtectedAndInternal, Accessibility.Protected, Accessibility.Internal, Accessibility.ProtectedOrInternal, Accessibility.Public }; 97 | var propertyAccessList = new[] { Accessibility.Private, Accessibility.ProtectedAndInternal, Accessibility.Protected, Accessibility.Internal, Accessibility.ProtectedOrInternal, Accessibility.Public }; 98 | 99 | var validCombinations = 100 | from hostContainerTypeAccess in hostContainerTypeAccessList 101 | from hostTypeAccess in hostTypeAccessList 102 | from propertyTypeAccess in propertyTypeAccessList 103 | from propertyAccess in propertyAccessList 104 | where ValidateAccessModifierCombination(hostContainerTypeAccess, hostTypeAccess, propertyTypeAccess, propertyAccess) 105 | select (hostContainerTypeAccess, hostTypeAccess, propertyTypeAccess, propertyAccess); 106 | 107 | #>return new[] 108 | { 109 | <# 110 | foreach (var (hostContainerTypeAccess, hostTypeAccess, propertyTypeAccess, propertyAccess) in validCombinations) 111 | {#> 112 | new object[] { Accessibility.<#= hostContainerTypeAccess #>, Accessibility.<#= hostTypeAccess #>, Accessibility.<#= propertyTypeAccess #>, Accessibility.<#= propertyAccess #> }, 113 | <# } #> 114 | }; 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator.Tests/BindFixture.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Collections.Immutable; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Reflection; 10 | using System.Runtime.CompilerServices; 11 | using System.Threading.Tasks; 12 | 13 | using Microsoft.CodeAnalysis; 14 | 15 | using ReactiveMarbles.PropertyChanged.SourceGenerator.Builders; 16 | 17 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.Tests; 18 | 19 | internal class BindFixture 20 | { 21 | private readonly BindHostBuilder _hostTypeInfo; 22 | private readonly CompilationUtil _compilation; 23 | private readonly Action _testOutputHelper; 24 | private Type _hostType; 25 | private Type _viewModelPropertyType; 26 | private Type _valuePropertyType; 27 | 28 | private BindFixture(BindHostBuilder hostTypeInfo, Action testOutputHelper) 29 | { 30 | _hostTypeInfo = hostTypeInfo; 31 | _testOutputHelper = testOutputHelper; 32 | _compilation = new(x => _testOutputHelper(x)); 33 | } 34 | 35 | public string Sources { get; private set; } = string.Empty; 36 | 37 | public static async Task Create(BindHostBuilder hostTypeInfo, Action testOutputHelper) 38 | { 39 | var bindFixture = new BindFixture(hostTypeInfo, testOutputHelper); 40 | await bindFixture.Initialize().ConfigureAwait(false); 41 | return bindFixture; 42 | } 43 | 44 | public Task Initialize() => _compilation.Initialize(); 45 | 46 | public void RunGenerator(BindHostBuilder hostTypeInfo, out ImmutableArray compilationDiagnostics, out ImmutableArray generatorDiagnostics, bool writeOutput = false, [CallerMemberName] string callerMemberName = null) 47 | { 48 | var sources = new[] { ("HostBuilder.cs", hostTypeInfo.BuildRoot()) }; 49 | 50 | Compilation afterCompilation = null; 51 | compilationDiagnostics = default; 52 | generatorDiagnostics = default; 53 | try 54 | { 55 | _compilation.RunGenerators(out compilationDiagnostics, out generatorDiagnostics, out var beforeCompilation, out afterCompilation, sources); 56 | var assembly = GetAssembly(afterCompilation); 57 | _hostType = assembly.GetType(_hostTypeInfo.GetTypeName()); 58 | _viewModelPropertyType = assembly.GetType(_hostTypeInfo.ViewModelPropertyTypeName); 59 | _valuePropertyType = assembly.GetType(_hostTypeInfo.PropertyTypeName); 60 | } 61 | catch (InvalidOperationException) 62 | { 63 | var compilationErrors = compilationDiagnostics.Where(x => x.Severity == DiagnosticSeverity.Error).Select(x => x.GetMessage()).ToList(); 64 | Sources = string.Join(Environment.NewLine, afterCompilation.SyntaxTrees.Select(x => x.ToString()).Where(x => !x.Contains("The implementation should have been generated."))); 65 | if (compilationErrors.Count > 0) 66 | { 67 | throw; 68 | } 69 | } 70 | finally 71 | { 72 | if (afterCompilation is not null && writeOutput) 73 | { 74 | var directory = Directory.CreateDirectory(Path.Combine("./erroroutput/bind", callerMemberName)); 75 | foreach (var source in afterCompilation.SyntaxTrees) 76 | { 77 | var fileName = Path.Combine(directory.FullName, Path.GetFileName(source.FilePath)); 78 | File.WriteAllText(fileName, source.ToString()); 79 | } 80 | } 81 | } 82 | } 83 | 84 | public BindHostProxy NewHostInstance() => new(CreateInstance(_hostType)); 85 | 86 | public WhenChangedHostProxy NewViewModelPropertyInstance() => new(CreateInstance(_viewModelPropertyType)); 87 | 88 | public object NewValuePropertyInstance() => CreateInstance(_valuePropertyType); 89 | 90 | private static object CreateInstance(Type type) => Activator.CreateInstance(type, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, null, null, null) ?? throw new InvalidOperationException("The value of the type cannot be null"); 91 | 92 | private static Assembly GetAssembly(Compilation compilation) 93 | { 94 | using var ms = new MemoryStream(); 95 | var result = compilation.Emit(ms); 96 | ms.Seek(0, SeekOrigin.Begin); 97 | return Assembly.Load(ms.ToArray()); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator.Tests/BindGeneratorTestsDiagnostics.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | using FluentAssertions; 9 | 10 | using Microsoft.CodeAnalysis; 11 | using Microsoft.VisualStudio.TestTools.UnitTesting; 12 | using ReactiveMarbles.PropertyChanged.SourceGenerator.Builders; 13 | 14 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.Tests; 15 | 16 | /// 17 | /// Bind generator tests. 18 | /// 19 | [TestClass] 20 | public sealed class BindGeneratorTestsDiagnostics 21 | { 22 | private const string BindPlaceholder = "[bind_invocation]"; 23 | private const string SourceTemplate = @" 24 | using System; 25 | using System.ComponentModel; 26 | using System.Linq.Expressions; 27 | 28 | public class View : INotifyPropertyChanged 29 | { 30 | public View() 31 | { 32 | [bind_invocation]; 33 | } 34 | 35 | public event PropertyChangedEventHandler PropertyChanged; 36 | public ViewModel ViewModel { get; } 37 | public View Child { get; } 38 | public string Value { get; } 39 | public View[] Children { get; } 40 | public string[] Values { get; } 41 | public Expression> ViewExpression => x => x.Value; 42 | public Expression> ViewModelExpression => x => x.Value; 43 | public Expression> GetViewExpression() => x => x.Value; 44 | public Expression> GetViewModelExpression() => x => x.Value; 45 | public View GetChild() => Child; 46 | public string GetValue() => Value; 47 | } 48 | 49 | public class ViewModel : INotifyPropertyChanged 50 | { 51 | public event PropertyChangedEventHandler PropertyChanged; 52 | public ViewModel Child { get; } 53 | public string Value { get; } 54 | public ViewModel[] Children { get; } 55 | public string[] Values { get; } 56 | public ViewModel GetChild() => Child; 57 | public string GetValue() => Value; 58 | } 59 | "; 60 | 61 | /// 62 | /// Gets or sets the test context. 63 | /// 64 | public TestContext TestContext { get; set; } 65 | 66 | /// 67 | /// The initialize. 68 | /// 69 | /// The context. 70 | /// A task to monitor the progress. 71 | [ClassInitialize] 72 | public static Task InitializeClass(TestContext context) => CommonTest.Initialize(context); 73 | 74 | /// 75 | /// Expression arguments may not be specified as a property pointing to the actual expression. 76 | /// Yes: this.Bind(ViewModel, x => x.Value, x => x.Value). 77 | /// No: this.Bind(ViewModel, ViewExpression, x => x.Value). 78 | /// 79 | [TestMethod] 80 | public void RXM001_PropertyInvocationUsedAsAnExpressionArgument() 81 | { 82 | var invocation = "this.Bind(ViewModel, ViewExpression, ViewModelExpression)"; 83 | var source = SourceTemplate.Replace(BindPlaceholder, invocation); 84 | AssertDiagnostic(source, DiagnosticWarnings.ExpressionMustBeInline); 85 | } 86 | 87 | /// 88 | /// Expression arguments may not be specified as a method pointing to the actual expression. 89 | /// Yes: this.Bind(ViewModel, x => x.Value, x => x.Value). 90 | /// No: this.Bind(ViewModel, GetViewExpression(), x => x.Value). 91 | /// 92 | [TestMethod] 93 | public void RXM001_MethodInvocationUsedAsAnExpressionArgument() 94 | { 95 | var invocation = "this.Bind(ViewModel, GetViewExpression(), GetViewModelExpression())"; 96 | var source = SourceTemplate.Replace(BindPlaceholder, invocation); 97 | AssertDiagnostic(source, DiagnosticWarnings.ExpressionMustBeInline); 98 | } 99 | 100 | private static void AssertDiagnostic(string source, DiagnosticDescriptor expectedDiagnostic) 101 | { 102 | Action act = () => CommonTest.CompilationUtil.CheckDiagnostics(("Diagnostics.cs", source), expectedDiagnostic); 103 | act.Should().Throw(); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator.Tests/CommonTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Microsoft.VisualStudio.TestTools.UnitTesting; 9 | using ReactiveMarbles.PropertyChanged.SourceGenerator.Builders; 10 | 11 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.Tests; 12 | 13 | internal static class CommonTest 14 | { 15 | private static CompilationUtil _compilationUtil; 16 | 17 | private static Func _compilationInitFunc = context => 18 | { 19 | _compilationUtil = new(context.WriteLine); 20 | return _compilationUtil.Initialize(); 21 | }; 22 | 23 | public static CompilationUtil CompilationUtil => _compilationUtil; 24 | 25 | public static async Task Initialize(TestContext testContext) 26 | { 27 | var func = Interlocked.Exchange(ref _compilationInitFunc, null); 28 | 29 | if (func is not null) 30 | { 31 | await func.Invoke(testContext).ConfigureAwait(false); 32 | } 33 | 34 | return _compilationUtil; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Runtime.CompilerServices; 6 | using Microsoft.VisualStudio.TestTools.UnitTesting; 7 | 8 | [assembly: InternalsVisibleTo("ReactiveMarbles.PropertyChanged.SourceGenerator.Benchmarks")] 9 | [assembly: Parallelize(Workers = 0, Scope = ExecutionScope.MethodLevel)] 10 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator.Tests/ReactiveMarbles.PropertyChanged.SourceGenerator.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | false 7 | $(NoWarn);SA1600 8 | preview 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | AccessibilityTestCases.cs 32 | TextTemplatingFileGenerator 33 | 34 | 35 | PreserveNewest 36 | 37 | 38 | 39 | 40 | 41 | True 42 | True 43 | AccessibilityTestCases.tt 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator.Tests/SampleTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Threading.Tasks; 10 | 11 | using FluentAssertions; 12 | 13 | using Microsoft.CodeAnalysis; 14 | using Microsoft.VisualStudio.TestTools.UnitTesting; 15 | using ReactiveMarbles.PropertyChanged.SourceGenerator.Builders; 16 | 17 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.Tests; 18 | 19 | /// 20 | /// Tests the sample project. 21 | /// 22 | [TestClass] 23 | public class SampleTests 24 | { 25 | /// 26 | /// Gets or sets the test context. 27 | /// 28 | public TestContext TestContext { get; set; } 29 | 30 | /// 31 | /// The initialize. 32 | /// 33 | /// The context. 34 | /// A task to monitor the progress. 35 | [ClassInitialize] 36 | public static Task InitializeClass(TestContext context) => CommonTest.Initialize(context); 37 | 38 | /// 39 | /// Runs the samples project. 40 | /// 41 | [TestMethod] 42 | public void TestSample() 43 | { 44 | var files = Directory.GetFiles("../../../../ReactiveMarbles.PropertyChanged.SourceGenerator.Sample/", "*.cs", new EnumerationOptions() { RecurseSubdirectories = true, IgnoreInaccessible = true, MatchType = MatchType.Simple }) 45 | .Where(x => !x.Contains("obj" + Path.DirectorySeparatorChar, StringComparison.InvariantCulture)); 46 | 47 | var sources = files.Select(x => (x, File.ReadAllText(x))).ToArray(); 48 | 49 | CommonTest.CompilationUtil.RunGenerators(out var compilationDiagnostics, out var generatorDiagnostics, out var beforeCompilation, out var afterCompilation, sources); 50 | 51 | var compilationErrors = compilationDiagnostics.Where(x => x.Severity == DiagnosticSeverity.Error).Select(x => x.GetMessage()).ToList(); 52 | if (compilationErrors.Count > 0) 53 | { 54 | var outputSources = string.Join(Environment.NewLine, afterCompilation.SyntaxTrees.Select(x => x.ToString()).Where(x => !x.Contains("The implementation should have been generated."))); 55 | TestContext.WriteLine(outputSources); 56 | throw new InvalidOperationException(string.Join('\n', compilationErrors)); 57 | } 58 | 59 | generatorDiagnostics.Where(x => x.Severity >= DiagnosticSeverity.Warning).Should().BeEmpty(); 60 | compilationDiagnostics.Where(x => x.Severity >= DiagnosticSeverity.Warning).Should().BeEmpty(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator.Tests/TestHelper.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | 10 | using FluentAssertions; 11 | 12 | using Microsoft.CodeAnalysis; 13 | 14 | using ReactiveMarbles.PropertyChanged.SourceGenerator.Builders; 15 | 16 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.Tests; 17 | 18 | internal static class TestHelper 19 | { 20 | public static void CheckDiagnostics(this CompilationUtil compilationUtil, (string FileName, string Source) source, DiagnosticDescriptor expectedDiagnostic) 21 | { 22 | compilationUtil.RunGenerators(out var compilationDiagnostics, out var generatorDiagnostics, out var compilation, out var newCompilation, source); 23 | var compilationErrors = compilationDiagnostics.Where(x => x.Severity == DiagnosticSeverity.Error).Select(x => x.GetMessage()).ToList(); 24 | 25 | compilationErrors.Should().BeEmpty(); 26 | generatorDiagnostics.Should().HaveCount(1); 27 | expectedDiagnostic.Should().Be(generatorDiagnostics[0].Descriptor); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator.Tests/WhenChangedFixture.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Collections.Immutable; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Reflection; 10 | using System.Runtime.CompilerServices; 11 | using System.Threading.Tasks; 12 | 13 | using Microsoft.CodeAnalysis; 14 | 15 | using ReactiveMarbles.PropertyChanged.SourceGenerator.Builders; 16 | 17 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.Tests; 18 | 19 | internal sealed class WhenChangedFixture : IDisposable 20 | { 21 | private readonly WhenChangedHostBuilder _hostTypeInfo; 22 | private readonly WhenChangedHostBuilder _receiverTypeInfo; 23 | private readonly Action _testLogger; 24 | private readonly CompilationUtil _compilation; 25 | private readonly (string FileName, string Source)[] _extraSources; 26 | private Type _hostType; 27 | private Type _receiverType; 28 | private Type _valuePropertyType; 29 | 30 | private WhenChangedFixture(WhenChangedHostBuilder hostTypeInfo, WhenChangedHostBuilder receiverTypeInfo, Action testLogger, (string FileName, string Source)[] extraSources) 31 | { 32 | _hostTypeInfo = hostTypeInfo; 33 | _receiverTypeInfo = receiverTypeInfo; 34 | _testLogger = testLogger; 35 | _compilation = new CompilationUtil(x => testLogger?.Invoke(x)); 36 | _extraSources = extraSources; 37 | } 38 | 39 | public string Sources { get; private set; } 40 | 41 | public static Task Create(WhenChangedHostBuilder hostTypeInfo, Action testOutputHelper, params (string FileName, string Source)[] extraSources) => 42 | Create(hostTypeInfo, hostTypeInfo, testOutputHelper, extraSources); 43 | 44 | public static async Task Create(WhenChangedHostBuilder hostTypeInfo, WhenChangedHostBuilder receiverTypeInfo, Action testOutputHelper, params (string FileName, string Source)[] extraSources) 45 | { 46 | var fixture = new WhenChangedFixture(hostTypeInfo, receiverTypeInfo, testOutputHelper, extraSources); 47 | await fixture.Initialize().ConfigureAwait(false); 48 | return fixture; 49 | } 50 | 51 | public Task Initialize() => _compilation.Initialize(); 52 | 53 | public void Dispose() => _compilation?.Dispose(); 54 | 55 | public void RunGenerator(out ImmutableArray compilationDiagnostics, out ImmutableArray generatorDiagnostics, bool writeOutput = false, [CallerMemberName] string callerMemberName = null) 56 | { 57 | var sources = _extraSources.Concat(new[] { (FileName: "receiver.cs", Source: _receiverTypeInfo.BuildRoot()) }); 58 | if (_receiverTypeInfo != _hostTypeInfo) 59 | { 60 | sources = sources.Concat(new[] { (FileName: "hosttype.cs", _hostTypeInfo.BuildRoot()) }); 61 | } 62 | 63 | Compilation afterCompilation = null; 64 | compilationDiagnostics = default; 65 | generatorDiagnostics = default; 66 | try 67 | { 68 | _compilation.RunGenerators(out compilationDiagnostics, out generatorDiagnostics, out var beforeCompilation, out afterCompilation, sources.ToArray()); 69 | var assembly = GetAssembly(afterCompilation); 70 | _hostType = assembly.GetType(_hostTypeInfo.GetTypeName()); 71 | _receiverType = assembly.GetType(_receiverTypeInfo.GetTypeName()); 72 | _valuePropertyType = assembly.GetType(_receiverTypeInfo.ValuePropertyTypeName); 73 | } 74 | catch (InvalidOperationException) 75 | { 76 | var compilationErrors = compilationDiagnostics.Where(x => x.Severity == DiagnosticSeverity.Error).Select(x => x.GetMessage()).ToList(); 77 | Sources = string.Join(Environment.NewLine, afterCompilation.SyntaxTrees.Select(x => x.ToString()).Where(x => !x.Contains("The implementation should have been generated."))); 78 | } 79 | 80 | if (afterCompilation is not null && writeOutput) 81 | { 82 | var directory = Directory.CreateDirectory(Path.Combine("./erroroutput/whenchanged", callerMemberName)); 83 | foreach (var source in afterCompilation.SyntaxTrees) 84 | { 85 | var fileName = Path.Combine(directory.FullName, Path.GetFileName(source.FilePath)); 86 | File.WriteAllText(fileName, source.ToString()); 87 | } 88 | } 89 | } 90 | 91 | public WhenChangedHostProxy NewHostInstance() => new(CreateInstance(_hostType)); 92 | 93 | public WhenChangedHostProxy NewReceiverInstance() => new(CreateInstance(_receiverType)); 94 | 95 | public object NewValuePropertyInstance() => CreateInstance(_valuePropertyType); 96 | 97 | private static object CreateInstance(Type type) => Activator.CreateInstance(type, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, null, null, null) ?? throw new InvalidOperationException("The value of the type cannot be null"); 98 | 99 | private static Assembly GetAssembly(Compilation compilation) 100 | { 101 | using var ms = new MemoryStream(); 102 | compilation.Emit(ms); 103 | ms.Seek(0, SeekOrigin.Begin); 104 | return Assembly.Load(ms.ToArray()); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator.Tests/xunit.runner.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", 3 | "maxParallelThreads": 4, 4 | "parallelizeTestCollections": true, 5 | "shadowCopy": false 6 | } 7 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator/AnalyzerReleases.Shipped.md: -------------------------------------------------------------------------------- 1 | ; Shipped analyzer releases 2 | ; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md 3 | 4 | ## Release 1.0 5 | 6 | ### New Rules 7 | 8 | Rule ID | Category | Severity | Notes 9 | --------|----------|----------|-------------------- 10 | RXM001 | Compiler | Error | 11 | RXM002 | Compiler | Error | 12 | RXM003 | Compiler | Error | 13 | RXM004 | Compiler | Error | 14 | RXM005 | Compiler | Error | 15 | RXM006 | Compiler | Error | 16 | RXM007 | Compiler | Error | 17 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator/AnalyzerReleases.Unshipped.md: -------------------------------------------------------------------------------- 1 | ; Unshipped analyzer release 2 | ; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md 3 | 4 | ### Removed Rules 5 | 6 | Rule ID | Category | Severity | Notes 7 | --------|----------|----------|-------------------- 8 | RXM001 | Compiler | Error | 9 | RXM002 | Compiler | Error | 10 | RXM003 | Compiler | Error | 11 | RXM004 | Compiler | Error | 12 | RXM005 | Compiler | Error | 13 | RXM006 | Compiler | Error | 14 | RXM007 | Compiler | Error | 15 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator/Comparers/SyntaxNodeComparer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | 8 | using Microsoft.CodeAnalysis; 9 | 10 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.Comparers; 11 | 12 | internal sealed class SyntaxNodeComparer : IComparer, IEqualityComparer 13 | { 14 | private SyntaxNodeComparer() 15 | { 16 | } 17 | 18 | public static SyntaxNodeComparer Default { get; } = new(); 19 | 20 | /// 21 | public int Compare(SyntaxNode? x, SyntaxNode? y) 22 | { 23 | if (x is null && y is null) 24 | { 25 | return 0; 26 | } 27 | 28 | if (x is null) 29 | { 30 | return 1; 31 | } 32 | 33 | if (y is null) 34 | { 35 | return -1; 36 | } 37 | 38 | return x.IsEquivalentTo(y) ? 0 : StringComparer.InvariantCulture.Compare(x.ToString(), y.ToString()); 39 | } 40 | 41 | /// 42 | public bool Equals(SyntaxNode? x, SyntaxNode? y) => x?.IsEquivalentTo(y) == true; 43 | 44 | /// 45 | public int GetHashCode(SyntaxNode obj) => obj.ToString().GetHashCode(); 46 | } 47 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator/Constants.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | // 5 | 6 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator; 7 | 8 | internal static partial class Constants 9 | { 10 | internal const string WhenChangedMethodName = "WhenChanged"; 11 | internal const string WhenChangingMethodName = "WhenChanging"; 12 | internal const string WhenExtensionClass = "NotifyPropertyExtensions"; 13 | 14 | internal const string BindTwoWayMethodName = "BindTwoWay"; 15 | internal const string BindOneWayMethodName = "BindOneWay"; 16 | internal const string BindExtensionClass = "BindingExtensions"; 17 | 18 | // file header stuff 19 | internal const string WarningDisable = @"//---------------------- 20 | // 21 | // Generated by ReactiveMarbles.PropertyChanged.SourceGenerator. DO NOT EDIT! 22 | // 23 | //---------------------- 24 | #pragma warning disable 25 | "; 26 | 27 | internal const string PreserveAttribute = @" 28 | #pragma warning disable 29 | [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] 30 | [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] 31 | [global::System.AttributeUsage (global::System.AttributeTargets.Class | global::System.AttributeTargets.Struct | global::System.AttributeTargets.Enum | global::System.AttributeTargets.Constructor | global::System.AttributeTargets.Method | global::System.AttributeTargets.Property | global::System.AttributeTargets.Field | global::System.AttributeTargets.Event | global::System.AttributeTargets.Interface | global::System.AttributeTargets.Delegate)] 32 | sealed class PreserveAttribute : global::System.Attribute 33 | { 34 | // 35 | // Fields 36 | // 37 | public bool AllMembers; 38 | public bool Conditional; 39 | }"; 40 | 41 | // attributes 42 | internal const string ExcludeFromCodeCoverageAttributeTypeName = "global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute"; 43 | internal const string DebuggerNonUserCodeAttributeTypeName = "global::System.Diagnostics.DebuggerNonUserCodeAttribute"; 44 | internal const string PreserveAttributeTypeName = "PreserveAttribute"; 45 | internal const string ObfuscationAttributeTypeName = "global::System.Reflection.Obfuscation"; 46 | internal const string EditorBrowsableTypeName = "global::System.ComponentModel.EditorBrowsable"; 47 | 48 | // variable names 49 | internal const string HostObservableVariable = "hostObs"; 50 | internal const string FromObjectVariable = "fromObject"; 51 | internal const string ThisObjectVariable = "this"; 52 | internal const string TargetObservableVariable = "targetObs"; 53 | 54 | // types 55 | internal const string StringTypeName = "global::System.String"; 56 | internal const string ObservableLinqTypeName = "global::System.Reactive.Linq.Observable"; 57 | internal const string ObservableExtensionsTypeName = "global::System.ObservableExtensions"; 58 | internal const string WhenChangedEventHandler = "global::System.ComponentModel.PropertyChangedEventHandler"; 59 | internal const string WhenChangingEventHandler = "global::System.ComponentModel.PropertyChangingEventHandler"; 60 | internal const string IObservableTypeName = "global::System.IObservable"; 61 | internal const string ImmediateSchedulerTypeName = "global::System.Reactive.Concurrency.ImmediateScheduler"; 62 | internal const string SystemDisposableTypeName = "global::System.IDisposable"; 63 | internal const string ReactiveDisposableTypeName = "global::System.Reactive.Disposables.Disposable"; 64 | internal const string ISchedulerTypeName = "global::System.Reactive.Concurrency.IScheduler"; 65 | internal const string CompositeDisposableTypeName = "global::System.Reactive.Disposables.CompositeDisposable"; 66 | internal const string FuncTypeName = "global::System.Func"; 67 | internal const string ExpressionTypeName = "global::System.Linq.Expressions.Expression"; 68 | internal const string CallerMemberAttributeTypeName = "global::System.Runtime.CompilerServices.CallerMemberNameAttribute"; 69 | internal const string CallerFilePathAttributeTypeName = "global::System.Runtime.CompilerServices.CallerFilePath"; 70 | internal const string CallerLineNumberAttributeTypeName = "global::System.Runtime.CompilerServices.CallerLineNumber"; 71 | internal const string InvalidOperationExceptionTypeName = "global::System.InvalidOperationException"; 72 | internal const string EditorBrowsableStateTypeName = "global::System.ComponentModel.EditorBrowsableState"; 73 | 74 | // method names 75 | internal const string EqualsMethod = "Equals"; 76 | internal const string ToStringMethod = "ToString"; 77 | internal const string SelectMethod = "Select"; 78 | internal const string SubscribeMethodName = "Subscribe"; 79 | internal const string ObserveOnMethodName = "ObserveOn"; 80 | internal const string CreateMethodName = "Create"; 81 | internal const string OnNextMethodName = "OnNext"; 82 | internal const string HandlerMethodName = "Handler"; 83 | internal const string SwitchMethodName = "Switch"; 84 | internal const string SkipMethodName = "Skip"; 85 | internal const string CombineLatestMethodName = "CombineLatest"; 86 | 87 | // events 88 | internal const string WhenChangedEventName = "PropertyChanged"; 89 | internal const string WhenChangingEventName = "PropertyChanging"; 90 | 91 | // parameter names 92 | internal const string SchedulerParameter = "scheduler"; 93 | internal const string HostToTargetConverterFuncParameter = "hostToTargetConv"; 94 | internal const string TargetToHostConverterFuncParameter = "targetToHostConv"; 95 | internal const string TargetParameter = "targetObject"; 96 | internal const string FromPropertyParameter = "fromProperty"; 97 | internal const string ToPropertyParameter = "toProperty"; 98 | internal const string LambdaSingleParameterName = "x"; 99 | internal const string ObserverParameterName = "observer"; 100 | internal const string SourceParameterName = "source"; 101 | internal const string SenderParameterName = "sender"; 102 | internal const string CallerMemberParameterName = "callerMember"; 103 | internal const string CallerFilePathParameterName = "callerFilePath"; 104 | internal const string CallerLineNumberParameterName = "callerLineNumber"; 105 | internal const string EventArgumentsParameterName = "e"; 106 | internal const string HandlerParameterName = "handler"; 107 | internal const string PropertyExpressionParameterName = "propertyExpression"; 108 | internal const string ConverterParameterName = "conversionFunc"; 109 | 110 | // property names 111 | internal const string InstancePropertyName = "Instance"; 112 | internal const string EmptyPropertyName = "Empty"; 113 | internal const string PropertyNamePropertyName = "PropertyName"; 114 | internal const string ParentPropertyName = "Parent"; 115 | 116 | // Enum member names 117 | internal const string NeverEnumMemberName = "Never"; 118 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator/DiagnosticWarnings.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using Microsoft.CodeAnalysis; 6 | 7 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator; 8 | 9 | internal static class DiagnosticWarnings 10 | { 11 | internal static readonly DiagnosticDescriptor ExpressionMustBeInline = new( 12 | "RXM001", 13 | "Expression chain must be inline", 14 | "The expression must be inline (e.g. not a variable or method invocation).", 15 | "Compiler", 16 | DiagnosticSeverity.Error, 17 | true); 18 | 19 | internal static readonly DiagnosticDescriptor OnlyPropertyAndFieldAccessAllowed = new( 20 | "RXM002", 21 | "Expression chain may only consist of property and field access", 22 | "The expression may only consist of field and property access", 23 | "Compiler", 24 | DiagnosticSeverity.Error, 25 | true); 26 | 27 | internal static readonly DiagnosticDescriptor LambdaParameterMustBeUsed = new( 28 | "RXM003", 29 | "Lambda parameter must be used in expression chain", 30 | "The lambda parameter must be used in the expression chain", 31 | "Compiler", 32 | DiagnosticSeverity.Error, 33 | true); 34 | 35 | internal static readonly DiagnosticDescriptor BindingIncorrectNumberParameters = new( 36 | "RXM004", 37 | "Must be both ViewModel and View expressions for Bind method", 38 | "Must be both ViewModel and View expressions for Bind method", 39 | "Compiler", 40 | DiagnosticSeverity.Error, 41 | true); 42 | 43 | internal static readonly DiagnosticDescriptor InvalidExpression = new( 44 | "RXM005", 45 | "The expression is not valid and does not point towards a valid item", 46 | "The expression is not valid and does not point towards a valid item", 47 | "Compiler", 48 | DiagnosticSeverity.Error, 49 | true); 50 | 51 | internal static readonly DiagnosticDescriptor UnableToGenerateExtension = new( 52 | "RXM006", 53 | "Unable to generate extension method", 54 | "Unable to generate extension method because the invocation involves one or more private/protected types or properties. Invoke via instance method, instead.", 55 | "Compiler", 56 | DiagnosticSeverity.Error, 57 | true); 58 | 59 | internal static readonly DiagnosticDescriptor InvalidNumberExpressions = new( 60 | "RXM007", 61 | "There are not enough expressions for Bind", 62 | "There are not enough expressions for Bind. There are {0} elements.", 63 | "Compiler", 64 | DiagnosticSeverity.Error, 65 | true); 66 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator/Generator.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | 10 | using Microsoft.CodeAnalysis; 11 | using Microsoft.CodeAnalysis.CSharp; 12 | using Microsoft.CodeAnalysis.CSharp.Syntax; 13 | using Microsoft.CodeAnalysis.Text; 14 | 15 | using ReactiveMarbles.PropertyChanged.SourceGenerator.Helpers; 16 | using ReactiveMarbles.PropertyChanged.SourceGenerator.MethodCreators; 17 | using ReactiveMarbles.PropertyChanged.SourceGenerator.MethodCreators.Transient; 18 | 19 | using static ReactiveMarbles.RoslynHelpers.SyntaxFactoryHelpers; 20 | 21 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator; 22 | 23 | /// 24 | /// The main source generator. 25 | /// 26 | [Generator] 27 | public class Generator : ISourceGenerator 28 | { 29 | /// 30 | public void Initialize(GeneratorInitializationContext context) => context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); 31 | 32 | /// 33 | public void Execute(GeneratorExecutionContext context) 34 | { 35 | try 36 | { 37 | var compilation = (CSharpCompilation)context.Compilation; 38 | var options = compilation.SyntaxTrees[0].Options as CSharpParseOptions; 39 | compilation = compilation.AddSyntaxTrees( 40 | CSharpSyntaxTree.ParseText(Constants.WhenExtensionClassSource, options), 41 | CSharpSyntaxTree.ParseText(Constants.BindExtensionClassSource, options)); 42 | context.AddSource("PropertyChanged.SourceGenerator.When.Stubs.g.cs", SourceText.From(Constants.WhenExtensionClassSource, Encoding.UTF8)); 43 | context.AddSource("PropertyChanged.SourceGenerator.PreserveAttribute.g.cs", SourceText.From(Constants.PreserveAttribute, Encoding.UTF8)); 44 | context.AddSource("PropertyChanged.SourceGenerator.Binding.Stubs.g.cs", SourceText.From(Constants.BindExtensionClassSource, Encoding.UTF8)); 45 | 46 | if (context.SyntaxReceiver is not SyntaxReceiver syntaxReceiver) 47 | { 48 | return; 49 | } 50 | 51 | WriteMembers(syntaxReceiver, compilation, context); 52 | } 53 | catch (Exception ex) 54 | { 55 | context.ReportDiagnostic(new DiagnosticDescriptor( 56 | "RXMERR", 57 | "Error has happened", 58 | $"Exception {ex}", 59 | "Compiler", 60 | DiagnosticSeverity.Error, 61 | true)); 62 | } 63 | } 64 | 65 | private static void WriteMembers(SyntaxReceiver syntaxReceiver, CSharpCompilation compilation, in GeneratorExecutionContext context) 66 | { 67 | var compilationData = MethodCreator.Generate(syntaxReceiver, compilation, context); 68 | 69 | foreach (var namespaceDatum in compilationData.GetPartials()) 70 | { 71 | var source = GenerateNamespaceSource(namespaceDatum, false); 72 | var namespaceName = string.IsNullOrWhiteSpace(namespaceDatum.NamespaceName) ? "global" : namespaceDatum.NamespaceName; 73 | context.AddSource("PropertyChanged.SourceGenerator.Partials." + namespaceName + ".cs", SourceText.From(source, Encoding.UTF8)); 74 | } 75 | 76 | var extensionClasses = compilationData.GetExtensionClasses().ToList(); 77 | 78 | context.AddSource("PropertyChanged.SourceGenerator.Extensions.cs", SourceText.From(GenerateClassesSource(extensionClasses, true), Encoding.UTF8)); 79 | } 80 | 81 | private static string GenerateNamespaceSource(NamespaceDatum namespaceEntry, bool isExtension) 82 | { 83 | var members = GenerateNamespaces(namespaceEntry, isExtension); 84 | var sb = new StringBuilder(Constants.WarningDisable); 85 | 86 | var compilationUnit = CompilationUnit(Array.Empty(), members, Array.Empty()); 87 | 88 | sb.AppendLine(compilationUnit.ToFullString()); 89 | 90 | return sb.ToString(); 91 | } 92 | 93 | private static string GenerateClassesSource(IReadOnlyCollection classes, bool isExtension) 94 | { 95 | var members = classes.Select(classEntry => GenerateClass(classEntry, isExtension)).Cast().ToList(); 96 | 97 | var sb = new StringBuilder(Constants.WarningDisable); 98 | 99 | var compilationUnit = CompilationUnit(Array.Empty(), members, Array.Empty()); 100 | 101 | sb.AppendLine(compilationUnit.ToFullString()); 102 | 103 | return sb.ToString(); 104 | } 105 | 106 | private static List GenerateNamespaces(NamespaceDatum namespaceEntry, bool isExtension) 107 | { 108 | var members = new List(); 109 | 110 | var globalNamespace = namespaceEntry.IsGlobal; 111 | 112 | var classDeclarations = !globalNamespace ? new() : members; 113 | members.AddRange(namespaceEntry.Classes.Select(classEntry => GenerateClass(classEntry, isExtension)).Cast()); 114 | 115 | if (!globalNamespace) 116 | { 117 | var namespaceDeclaration = NamespaceDeclaration(namespaceEntry.NamespaceName, classDeclarations, false); 118 | members.Add(namespaceDeclaration); 119 | } 120 | 121 | return members; 122 | } 123 | 124 | private static ClassDeclarationSyntax GenerateClass(ClassDatum classEntry, bool isExtension) 125 | { 126 | var accessibility = isExtension ? 127 | new[] { SyntaxKind.InternalKeyword, SyntaxKind.StaticKeyword, SyntaxKind.PartialKeyword } : 128 | classEntry.AccessibilityModifier.GetAccessibilityTokens().Concat(new[] { SyntaxKind.PartialKeyword }).ToArray(); 129 | 130 | var className = classEntry.ClassName; 131 | var methods = classEntry.MethodData; 132 | 133 | var currentClass = ClassDeclaration(className, Array.Empty(), accessibility, methods.Select(x => x.Expression).ToList(), 1); 134 | 135 | foreach (var ancestor in classEntry.Ancestors) 136 | { 137 | accessibility = ancestor.AccessibilityModifier.GetAccessibilityTokens().Concat(new[] { SyntaxKind.PartialKeyword }).ToArray(); 138 | currentClass = ClassDeclaration(ancestor.ClassName, accessibility, new[] { currentClass }, 0); 139 | } 140 | 141 | return currentClass; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator/Helpers/AccessibilityExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using Microsoft.CodeAnalysis; 6 | 7 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.Helpers; 8 | 9 | internal static class AccessibilityExtensions 10 | { 11 | internal static bool IsPrivateOrProtected(this Accessibility accessibility) => 12 | accessibility is <= Accessibility.Protected or Accessibility.ProtectedOrInternal; 13 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator/Helpers/Extensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Collections.Immutable; 8 | using System.Linq; 9 | 10 | using Microsoft.CodeAnalysis; 11 | 12 | using ReactiveMarbles.PropertyChanged.SourceGenerator.MethodCreators.Transient; 13 | 14 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.Helpers; 15 | 16 | internal static class Extensions 17 | { 18 | public static List GetAncestorsClassDatum(this ITypeSymbol inputType) => 19 | GetAncestors(inputType).Select(x => new ClassDatum(x.Name, x.DeclaredAccessibility, false, Array.Empty())).ToList(); 20 | 21 | public static IEnumerable GetThisAndAncestors(this ITypeSymbol inputType) 22 | { 23 | var containingType = inputType; 24 | 25 | while (containingType is not null) 26 | { 27 | yield return containingType; 28 | containingType = containingType.ContainingType; 29 | } 30 | } 31 | 32 | public static IEnumerable GetAncestors(this ITypeSymbol inputType) 33 | { 34 | var containingType = inputType.ContainingType; 35 | 36 | while (containingType is not null) 37 | { 38 | yield return containingType; 39 | containingType = containingType.ContainingType; 40 | } 41 | } 42 | 43 | public static void ReportDiagnostic(this in GeneratorExecutionContext context, DiagnosticDescriptor descriptor, Location? location = null, params object[] items) => context.ReportDiagnostic(Diagnostic.Create(descriptor, location, items)); 44 | 45 | public static Accessibility GetMinVisibility(this IReadOnlyList typeSymbols) 46 | { 47 | var inputTypeAccess = typeSymbols[0].GetVisibility(); 48 | var accessibility = Accessibility.Public; 49 | var oneOrMoreOfTheOutputTypesIsInternal = false; 50 | 51 | for (var i = 1; i < typeSymbols.Count; ++i) 52 | { 53 | var typeAccess = typeSymbols[i].GetVisibility(); 54 | if (typeAccess < accessibility) 55 | { 56 | accessibility = typeAccess; 57 | } 58 | 59 | if (typeAccess == Accessibility.Internal) 60 | { 61 | oneOrMoreOfTheOutputTypesIsInternal = true; 62 | } 63 | } 64 | 65 | if (inputTypeAccess == Accessibility.Protected && oneOrMoreOfTheOutputTypesIsInternal && accessibility > Accessibility.Private) 66 | { 67 | accessibility = Accessibility.Internal; 68 | } 69 | 70 | return accessibility; 71 | } 72 | 73 | public static Accessibility GetVisibility(this ITypeSymbol typeSymbol) 74 | { 75 | var accessibility = GetVisibilityHelper(typeSymbol); 76 | if (accessibility == Accessibility.Protected && typeSymbol.DeclaredAccessibility == Accessibility.Internal) 77 | { 78 | return Accessibility.Internal; 79 | } 80 | 81 | return accessibility; 82 | } 83 | 84 | private static Accessibility GetVisibilityHelper(this ITypeSymbol typeSymbol) 85 | { 86 | var accessibility = typeSymbol.DeclaredAccessibility; 87 | typeSymbol = typeSymbol.ContainingType; 88 | 89 | while (typeSymbol is not null) 90 | { 91 | if (typeSymbol.DeclaredAccessibility < accessibility) 92 | { 93 | accessibility = typeSymbol.DeclaredAccessibility; 94 | } 95 | 96 | typeSymbol = typeSymbol.ContainingType; 97 | } 98 | 99 | return accessibility; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator/Helpers/RoslynExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using Microsoft.CodeAnalysis; 8 | using Microsoft.CodeAnalysis.CSharp; 9 | 10 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.Helpers; 11 | 12 | internal static class RoslynExtensions 13 | { 14 | public static IEnumerable GetAccessibilityTokens(this Accessibility accessibility) => accessibility switch 15 | { 16 | Accessibility.Public => new[] { SyntaxKind.PublicKeyword }, 17 | Accessibility.Internal => new[] { SyntaxKind.InternalKeyword }, 18 | Accessibility.Private => new[] { SyntaxKind.PrivateKeyword }, 19 | Accessibility.NotApplicable => Array.Empty(), 20 | Accessibility.ProtectedAndInternal => new[] { SyntaxKind.PrivateKeyword, SyntaxKind.ProtectedKeyword }, 21 | Accessibility.Protected => new[] { SyntaxKind.ProtectedKeyword }, 22 | Accessibility.ProtectedOrInternal => new[] { SyntaxKind.ProtectedKeyword, SyntaxKind.InternalKeyword }, 23 | _ => Array.Empty(), 24 | }; 25 | 26 | public static string ToNamespaceName(this INamespaceSymbol symbol) 27 | { 28 | var name = symbol.ToDisplayString(); 29 | 30 | return name.Equals("") ? string.Empty : name; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator/Helpers/SourceHelpers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using Microsoft.CodeAnalysis.CSharp; 6 | using Microsoft.CodeAnalysis.CSharp.Syntax; 7 | using static ReactiveMarbles.RoslynHelpers.SyntaxFactoryHelpers; 8 | 9 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.Helpers; 10 | 11 | internal static class SourceHelpers 12 | { 13 | /// 14 | /// Generates: 15 | /// [source].WhenChanged(expressionName). 16 | /// 17 | /// The method name. 18 | /// The expression. 19 | /// The source variable. 20 | /// The invocation. 21 | public static InvocationExpressionSyntax InvokeWhenChanged(string methodName, string expressionName, string source) => 22 | InvocationExpression( 23 | MemberAccessExpression( 24 | SyntaxKind.SimpleMemberAccessExpression, 25 | IdentifierName(source), 26 | methodName), 27 | new[] 28 | { 29 | Argument(expressionName), 30 | Argument(Constants.CallerMemberParameterName), 31 | Argument(Constants.CallerFilePathParameterName), 32 | Argument(Constants.CallerLineNumberParameterName), 33 | }); 34 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator/IsExternalInit.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.ComponentModel; 6 | 7 | // ReSharper disable once CheckNamespace 8 | namespace System.Runtime.CompilerServices; 9 | 10 | /// 11 | /// Reserved to be used by the compiler for tracking metadata. 12 | /// This class should not be used by developers in source code. 13 | /// 14 | [EditorBrowsable(EditorBrowsableState.Never)] 15 | internal static class IsExternalInit 16 | { 17 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator/MethodCreators/MethodCreator.BindOneWay.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | using Microsoft.CodeAnalysis; 9 | using Microsoft.CodeAnalysis.CSharp; 10 | using Microsoft.CodeAnalysis.CSharp.Syntax; 11 | 12 | using ReactiveMarbles.PropertyChanged.SourceGenerator.Helpers; 13 | using ReactiveMarbles.PropertyChanged.SourceGenerator.MethodCreators.Transient; 14 | 15 | using static ReactiveMarbles.RoslynHelpers.SyntaxFactoryHelpers; 16 | 17 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.MethodCreators; 18 | 19 | internal static partial class MethodCreator 20 | { 21 | private static List CreateOneWayBindStatements(in ExpressionArgument hostExpressionArgument, in ExpressionArgument targetExpressionArgument, bool hasConverters, bool isExtension) 22 | { 23 | var statements = new List(); 24 | 25 | var hostOutputType = hostExpressionArgument.OutputType.ToDisplayString(); 26 | 27 | var fromName = isExtension ? Constants.FromObjectVariable : Constants.ThisObjectVariable; 28 | 29 | // generates: var hostObs = fromObject.WhenChanged(fromProperty); 30 | var observableChain = SourceHelpers.InvokeWhenChanged(Constants.WhenChangedMethodName, Constants.FromPropertyParameter, fromName); 31 | 32 | statements.Add(LocalDeclarationStatement(VariableDeclaration(GenericName(Constants.IObservableTypeName, new[] { IdentifierName(hostOutputType) }), new[] { VariableDeclarator(Constants.HostObservableVariable, EqualsValueClause(observableChain)) }))); 33 | 34 | if (hasConverters) 35 | { 36 | // generates: hostObs = hostObs.Select(hostToTargetConv); 37 | statements.Add(ExpressionStatement( 38 | AssignmentExpression( 39 | SyntaxKind.SimpleAssignmentExpression, 40 | Constants.HostObservableVariable, 41 | InvocationExpression( 42 | MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, Constants.HostObservableVariable, Constants.SelectMethod), 43 | new[] 44 | { 45 | Argument(Constants.HostToTargetConverterFuncParameter) 46 | })))); 47 | } 48 | 49 | // generates: scheduler = scheduler ?? ImmediateScheduler.Instance; 50 | statements.Add(ExpressionStatement( 51 | AssignmentExpression( 52 | SyntaxKind.SimpleAssignmentExpression, 53 | Constants.SchedulerParameter, 54 | BinaryExpression( 55 | SyntaxKind.CoalesceExpression, 56 | Constants.SchedulerParameter, 57 | MemberAccessExpression( 58 | SyntaxKind.SimpleMemberAccessExpression, 59 | Constants.ImmediateSchedulerTypeName, 60 | Constants.InstancePropertyName))))); 61 | 62 | // generates: if (scheduler != ImmediateScheduler.Instance) { ... } 63 | IfStatement( 64 | BinaryExpression( 65 | SyntaxKind.NotEqualsExpression, 66 | Constants.SchedulerParameter, 67 | MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, Constants.ImmediateSchedulerTypeName, Constants.InstancePropertyName)), 68 | Block( 69 | new[] 70 | { 71 | // generates: hostObs = hostObs.ObserveOn(scheduler); 72 | ExpressionStatement( 73 | AssignmentExpression( 74 | SyntaxKind.SimpleAssignmentExpression, 75 | Constants.HostObservableVariable, 76 | InvocationExpression( 77 | MemberAccessExpression( 78 | SyntaxKind.SimpleMemberAccessExpression, 79 | Constants.HostObservableVariable, 80 | Constants.ObserveOnMethodName), 81 | new[] { Argument(Constants.SchedulerParameter) }))), 82 | }, 83 | isExtension ? 3 : 4)); 84 | 85 | statements.Add(ReturnStatement(InvocationExpression( 86 | MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, Constants.ObservableExtensionsTypeName, Constants.SubscribeMethodName), 87 | new[] 88 | { 89 | Argument(Constants.HostObservableVariable), 90 | Argument(SimpleLambdaExpression( 91 | Parameter(Constants.LambdaSingleParameterName), 92 | AssignmentExpression( 93 | SyntaxKind.SimpleAssignmentExpression, 94 | MemberAccessExpression( 95 | SyntaxKind.SimpleMemberAccessExpression, 96 | Constants.TargetParameter, 97 | targetExpressionArgument.ExpressionChain[targetExpressionArgument.ExpressionChain.Count - 1].Name), 98 | Constants.LambdaSingleParameterName))), 99 | }))); 100 | 101 | return statements; 102 | } 103 | 104 | private static MethodDeclarationSyntax CreateBindOneWayMethod( 105 | string hostInputType, 106 | string hostOutputType, 107 | string targetInputType, 108 | string targetOutputType, 109 | bool isExtension, 110 | bool hasConverters, 111 | Accessibility accessibility, 112 | List statements) 113 | { 114 | var modifiers = accessibility.GetAccessibilityTokens().ToList(); 115 | 116 | var parameterList = new List(); 117 | 118 | if (isExtension) 119 | { 120 | modifiers.Add(SyntaxKind.StaticKeyword); 121 | parameterList.Add(Parameter(hostInputType, Constants.FromObjectVariable, new[] { SyntaxKind.ThisKeyword })); 122 | } 123 | 124 | parameterList.Add(Parameter(targetInputType, Constants.TargetParameter)); 125 | 126 | parameterList.Add(Parameter(GetExpressionFunc(hostInputType, hostOutputType), Constants.FromPropertyParameter)); 127 | parameterList.Add(Parameter(GetExpressionFunc(targetInputType, targetOutputType), Constants.ToPropertyParameter)); 128 | 129 | if (hasConverters) 130 | { 131 | parameterList.Add( 132 | Parameter( 133 | GenericName( 134 | Constants.FuncTypeName, 135 | new[] { IdentifierName(hostInputType), IdentifierName(targetOutputType) }), 136 | Constants.HostToTargetConverterFuncParameter)); 137 | } 138 | 139 | parameterList.Add(Parameter(Constants.ISchedulerTypeName, Constants.SchedulerParameter, EqualsValueClause(NullLiteral()))); 140 | 141 | parameterList.AddRange(CallerMembersParameters()); 142 | 143 | statements.Add(ThrowStatement(ObjectCreationExpression(Constants.InvalidOperationExceptionTypeName, new[] { Argument("\"No valid expression found.\"") }), isExtension ? 2 : 3)); 144 | 145 | var body = Block(statements, isExtension ? 1 : 2); 146 | 147 | return MethodDeclaration(GetMethodAttributes(), modifiers, Constants.SystemDisposableTypeName, Constants.BindOneWayMethodName, parameterList, 1, body); 148 | } 149 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator/MethodCreators/MethodCreator.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Collections.Generic; 6 | 7 | using Microsoft.CodeAnalysis; 8 | using Microsoft.CodeAnalysis.CSharp; 9 | using Microsoft.CodeAnalysis.CSharp.Syntax; 10 | 11 | using ReactiveMarbles.PropertyChanged.SourceGenerator.MethodCreators.Transient; 12 | 13 | using static ReactiveMarbles.RoslynHelpers.SyntaxFactoryHelpers; 14 | 15 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.MethodCreators; 16 | 17 | internal static partial class MethodCreator 18 | { 19 | public static CompilationDatum Generate(SyntaxReceiver syntaxReceiver, CSharpCompilation compilation, in GeneratorExecutionContext context) 20 | { 21 | var compilationData = new CompilationDatum(); 22 | var whenChangedData = new List(); 23 | 24 | GenerateBind(syntaxReceiver.BindOneWay, compilation, Constants.BindOneWayMethodName, whenChangedData, compilationData, context); 25 | GenerateBind(syntaxReceiver.BindTwoWay, compilation, Constants.BindTwoWayMethodName, whenChangedData, compilationData, context); 26 | 27 | GenerateWhenMetadata(syntaxReceiver.WhenChanged, compilation, Constants.WhenChangedMethodName, whenChangedData, context); 28 | GenerateWhenMetadata(syntaxReceiver.WhenChanging, compilation, Constants.WhenChangingMethodName, whenChangedData, context); 29 | 30 | GenerateWhenMethods(whenChangedData, compilationData); 31 | 32 | return compilationData; 33 | } 34 | 35 | private static List GetMethodAttributes() => new() 36 | { 37 | AttributeList(Attribute(Constants.ExcludeFromCodeCoverageAttributeTypeName)), 38 | AttributeList(Attribute(Constants.DebuggerNonUserCodeAttributeTypeName)), 39 | AttributeList(Attribute(Constants.PreserveAttributeTypeName, new[] { AttributeArgument(NameEquals(IdentifierName("AllMembers")), LiteralExpression(SyntaxKind.TrueLiteralExpression)) })), 40 | AttributeList(Attribute(Constants.ObfuscationAttributeTypeName, new[] { AttributeArgument(NameEquals(IdentifierName("Exclude")), LiteralExpression(SyntaxKind.TrueLiteralExpression)) })), 41 | AttributeList(Attribute(Constants.EditorBrowsableTypeName, new[] { AttributeArgument(MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, Constants.EditorBrowsableStateTypeName, Constants.NeverEnumMemberName)) })), 42 | }; 43 | 44 | private static IEnumerable CallerMembersParameters() => 45 | new[] 46 | { 47 | Parameter(new[] { AttributeList(Attribute(Constants.CallerMemberAttributeTypeName)) }, "string", Constants.CallerMemberParameterName, EqualsValueClause(NullLiteral())), 48 | Parameter(new[] { AttributeList(Attribute(Constants.CallerFilePathAttributeTypeName)) }, "string", Constants.CallerFilePathParameterName, EqualsValueClause(NullLiteral())), 49 | Parameter(new[] { AttributeList(Attribute(Constants.CallerLineNumberAttributeTypeName)) }, "int", Constants.CallerLineNumberParameterName, EqualsValueClause(LiteralExpression(0))), 50 | }; 51 | 52 | private static GenericNameSyntax GetExpressionFunc(string inputType, string returnType) => 53 | GenericName( 54 | Constants.ExpressionTypeName, 55 | new[] 56 | { 57 | GenericName( 58 | Constants.FuncTypeName, 59 | new[] 60 | { 61 | IdentifierName(inputType), 62 | IdentifierName(returnType), 63 | }), 64 | }); 65 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator/MethodCreators/Transient/BindStatementsDatum.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using Microsoft.CodeAnalysis; 7 | 8 | #pragma warning disable SA1313 // Use lower case -- Not working for record structs. 9 | 10 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.MethodCreators.Transient; 11 | 12 | internal sealed record BindStatementsDatum(ExpressionArgument HostArgument, ExpressionArgument TargetArgument, string MethodName, bool HasConverters, Accessibility ClassAccessibilty, Accessibility MethodAccessibility, SemanticModel Model) 13 | { 14 | /// 15 | public override int GetHashCode() 16 | { 17 | // Allow arithmetic overflow, numbers will just "wrap around" 18 | unchecked 19 | { 20 | var hashCode = 1430287; 21 | 22 | hashCode *= 7302013 ^ HasConverters.GetHashCode(); 23 | hashCode *= 7302013 ^ MethodAccessibility.GetHashCode(); 24 | hashCode *= 7302013 ^ HostArgument.GetHashCode(); 25 | hashCode *= 7302013 ^ TargetArgument.GetHashCode(); 26 | hashCode *= 7302013 ^ MethodName.GetHashCode(); 27 | 28 | return hashCode; 29 | } 30 | } 31 | 32 | public bool Equals(BindStatementsDatum? other) 33 | { 34 | if (other is null) 35 | { 36 | return false; 37 | } 38 | 39 | if (HasConverters != other.HasConverters) 40 | { 41 | return false; 42 | } 43 | 44 | if (MethodAccessibility != other.MethodAccessibility) 45 | { 46 | return false; 47 | } 48 | 49 | if (!HostArgument.Equals(other.HostArgument)) 50 | { 51 | return false; 52 | } 53 | 54 | return TargetArgument.Equals(other.TargetArgument) && MethodName.Equals(other.MethodName, StringComparison.InvariantCulture); 55 | } 56 | 57 | public override string ToString() => 58 | $"{MethodAccessibility.ToString()} IObservable<{TargetArgument.OutputType}> {MethodName}<{HostArgument.InputType}, {TargetArgument.OutputType}>(this {TargetArgument.InputType} input, {HostArgument.LambdaBodyString} inputExpression, {TargetArgument.LambdaBodyString} outputExpression)"; 59 | } 60 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator/MethodCreators/Transient/ClassDatum.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using Microsoft.CodeAnalysis; 8 | 9 | #pragma warning disable SA1313 // Use lower case -- Not working for record structs. 10 | 11 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.MethodCreators.Transient; 12 | 13 | internal sealed record ClassDatum(string ClassName, Accessibility AccessibilityModifier, bool IsExtension, IReadOnlyList Ancestors) 14 | { 15 | public HashSet MethodData { get; } = new(); 16 | 17 | /// 18 | public override int GetHashCode() 19 | { 20 | // Allow arithmetic overflow, numbers will just "wrap around" 21 | unchecked 22 | { 23 | var hashCode = 1430287; 24 | 25 | hashCode *= 7302013 ^ IsExtension.GetHashCode(); 26 | hashCode *= 7302013 ^ StringComparer.InvariantCulture.GetHashCode(ClassName); 27 | hashCode *= 7302013 ^ AccessibilityModifier.GetHashCode(); 28 | 29 | return hashCode; 30 | } 31 | } 32 | 33 | public bool Equals(ClassDatum? other) 34 | { 35 | if (other is null) 36 | { 37 | return false; 38 | } 39 | 40 | if (!StringComparer.InvariantCulture.Equals(other.ClassName, ClassName)) 41 | { 42 | return false; 43 | } 44 | 45 | if (IsExtension != other.IsExtension) 46 | { 47 | return false; 48 | } 49 | 50 | return AccessibilityModifier == other.AccessibilityModifier; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator/MethodCreators/Transient/CompilationDatum.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using Microsoft.CodeAnalysis; 9 | 10 | using ReactiveMarbles.PropertyChanged.SourceGenerator.Helpers; 11 | 12 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.MethodCreators.Transient; 13 | 14 | internal sealed class CompilationDatum 15 | { 16 | private readonly Dictionary _extensionNamespaces = new(); 17 | private readonly Dictionary _partialsNamespaces = new(); 18 | 19 | public NamespaceDatum GetNamespace(string namespaceName, bool isExtension) 20 | { 21 | var isGlobal = string.IsNullOrWhiteSpace(namespaceName); 22 | 23 | var dictionary = isExtension ? _extensionNamespaces : _partialsNamespaces; 24 | 25 | if (!dictionary.TryGetValue(namespaceName, out var namespaceDatum)) 26 | { 27 | namespaceDatum = new(namespaceName, isGlobal); 28 | dictionary[namespaceName] = namespaceDatum; 29 | } 30 | 31 | return namespaceDatum; 32 | } 33 | 34 | public ClassDatum GetClass(ITypeSymbol symbol, Accessibility classAccessibility, bool isExtension, string extensionClass) 35 | { 36 | var namespaceName = isExtension ? string.Empty : symbol.ContainingNamespace.ToNamespaceName(); 37 | var className = isExtension ? extensionClass : symbol.Name; 38 | var ancestors = isExtension ? Array.Empty() : (IReadOnlyList)symbol.GetAncestorsClassDatum(); 39 | var fileDatum = GetNamespace(namespaceName, isExtension); 40 | 41 | var dictionary = fileDatum.ClassDictionary; 42 | 43 | if (dictionary.TryGetValue(className, out var classMethodDatum)) 44 | { 45 | return classMethodDatum; 46 | } 47 | 48 | classMethodDatum = new(className, classAccessibility, isExtension, ancestors); 49 | dictionary.Add(className, classMethodDatum); 50 | 51 | return classMethodDatum; 52 | } 53 | 54 | public IEnumerable GetExtensions() => _extensionNamespaces.Values; 55 | 56 | public IEnumerable GetPartials() => _partialsNamespaces.Values; 57 | 58 | public IEnumerable GetExtensionClasses() => GetExtensions().SelectMany(namespaceDefinition => namespaceDefinition.Classes); 59 | } 60 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator/MethodCreators/Transient/ExpressionArgument.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Collections.Generic; 6 | 7 | using Microsoft.CodeAnalysis; 8 | 9 | #pragma warning disable SA1313 // Use lower case -- Not working for record structs. 10 | 11 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.MethodCreators.Transient; 12 | 13 | internal sealed record ExpressionArgument(string LambdaBodyString, IReadOnlyList ExpressionChain, ITypeSymbol InputType, ITypeSymbol OutputType, bool ContainsPrivateOrProtectedMember) : IComparer 14 | { 15 | public int Compare(ExpressionArgument x, ExpressionArgument y) 16 | { 17 | var inputCompare = TypeSymbolComparer.Default.Compare(x.InputType, y.InputType); 18 | 19 | return inputCompare != 0 ? inputCompare : TypeSymbolComparer.Default.Compare(x.OutputType, y.OutputType); 20 | } 21 | 22 | public bool Equals(ExpressionArgument? other) => other is not null && 23 | TypeSymbolComparer.Default.Equals(InputType, other.InputType) && 24 | TypeSymbolComparer.Default.Equals(OutputType, other.OutputType); 25 | 26 | public override int GetHashCode() 27 | { 28 | unchecked 29 | { 30 | var hashCode = 1230885993; 31 | hashCode = (hashCode * -1521134295) + TypeSymbolComparer.Default.GetHashCode(InputType); 32 | hashCode = (hashCode * -1521134295) + TypeSymbolComparer.Default.GetHashCode(OutputType); 33 | 34 | return hashCode; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator/MethodCreators/Transient/ExpressionChain.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using Microsoft.CodeAnalysis; 6 | 7 | #pragma warning disable SA1313 // Use lower case -- Not working for record structs. 8 | 9 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.MethodCreators.Transient; 10 | 11 | internal sealed record ExpressionChain(string Name, ITypeSymbol InputType, ITypeSymbol OutputType) 12 | { 13 | public bool Equals(ExpressionChain? other) 14 | { 15 | if (other is null) 16 | { 17 | return false; 18 | } 19 | 20 | if (!string.Equals(Name, other.Name, System.StringComparison.InvariantCulture)) 21 | { 22 | return false; 23 | } 24 | 25 | return TypeSymbolComparer.Default.Equals(InputType, other.InputType) && TypeSymbolComparer.Default.Equals(OutputType, other.OutputType); 26 | } 27 | 28 | public override int GetHashCode() 29 | { 30 | // Allow arithmetic overflow, numbers will just "wrap around" 31 | unchecked 32 | { 33 | var hashCode = 1430287; 34 | 35 | hashCode *= 7302013 ^ Name.GetHashCode(); 36 | hashCode *= 7302013 ^ TypeSymbolComparer.Default.GetHashCode(InputType); 37 | hashCode *= 7302013 ^ TypeSymbolComparer.Default.GetHashCode(OutputType); 38 | 39 | return hashCode; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator/MethodCreators/Transient/MethodDatum.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using Microsoft.CodeAnalysis; 6 | using Microsoft.CodeAnalysis.CSharp.Syntax; 7 | 8 | using ReactiveMarbles.PropertyChanged.SourceGenerator.Comparers; 9 | 10 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.MethodCreators.Transient; 11 | 12 | internal record MethodDatum(Accessibility ClassAccessibility, MethodDeclarationSyntax Expression) 13 | { 14 | /// 15 | public override int GetHashCode() 16 | { 17 | // Allow arithmetic overflow, numbers will just "wrap around" 18 | unchecked 19 | { 20 | var hashCode = 1430287; 21 | 22 | hashCode *= 7302013 ^ ClassAccessibility.GetHashCode(); 23 | hashCode *= 7302013 ^ SyntaxNodeComparer.Default.GetHashCode(Expression); 24 | 25 | return hashCode; 26 | } 27 | } 28 | 29 | public virtual bool Equals(MethodDatum? other) 30 | { 31 | if (other is null) 32 | { 33 | return false; 34 | } 35 | 36 | if (!SyntaxNodeComparer.Default.Equals(other.Expression, Expression)) 37 | { 38 | return false; 39 | } 40 | 41 | return ClassAccessibility == other.ClassAccessibility; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator/MethodCreators/Transient/MultiWhenStatementsDatum.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using Microsoft.CodeAnalysis; 9 | 10 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.MethodCreators.Transient; 11 | 12 | internal record MultiWhenStatementsDatum(string MethodName, IReadOnlyList Arguments, bool IsExtensionMethod, Accessibility ClassAccessibility, ITypeSymbol InputType, ITypeSymbol OutputType, IReadOnlyList TypeArguments) 13 | { 14 | public override int GetHashCode() 15 | { 16 | // Allow arithmetic overflow, numbers will just "wrap around" 17 | unchecked 18 | { 19 | var hashCode = 1430287; 20 | var result = hashCode; 21 | foreach (var argument in Arguments) 22 | { 23 | result *= 7302013 ^ argument.GetHashCode(); 24 | } 25 | 26 | hashCode *= result; 27 | 28 | hashCode *= 7302013 ^ MethodName.GetHashCode(); 29 | hashCode *= 7302013 ^ ClassAccessibility.GetHashCode(); 30 | hashCode *= 7302013 ^ IsExtensionMethod.GetHashCode(); 31 | 32 | return hashCode; 33 | } 34 | } 35 | 36 | public virtual bool Equals(MultiWhenStatementsDatum? other) 37 | { 38 | if (other is null) 39 | { 40 | return false; 41 | } 42 | 43 | if (other.Arguments.Count != Arguments.Count) 44 | { 45 | return false; 46 | } 47 | 48 | if (!other.MethodName.Equals(MethodName, StringComparison.InvariantCulture)) 49 | { 50 | return false; 51 | } 52 | 53 | if (Arguments.Where((t, i) => !t.Equals(other.Arguments[i])).Any()) 54 | { 55 | return false; 56 | } 57 | 58 | if (IsExtensionMethod != other.IsExtensionMethod) 59 | { 60 | return false; 61 | } 62 | 63 | return ClassAccessibility == other.ClassAccessibility; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator/MethodCreators/Transient/NamespaceDatum.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | 8 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.MethodCreators.Transient; 9 | 10 | internal sealed record NamespaceDatum(string NamespaceName, bool IsGlobal) 11 | { 12 | public Dictionary ClassDictionary { get; } = new(); 13 | 14 | public IEnumerable Classes => ClassDictionary.Values; 15 | 16 | public bool Equals(NamespaceDatum? other) => other?.NamespaceName.Equals(NamespaceName, StringComparison.InvariantCulture) == true; 17 | 18 | public override int GetHashCode() => NamespaceName.GetHashCode(); 19 | } 20 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator/MethodCreators/Transient/WhenStatementsDatum.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | 7 | #pragma warning disable SA1313 // Use lower case -- Not working for record structs. 8 | 9 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator.MethodCreators.Transient; 10 | 11 | internal sealed record WhenStatementsDatum(ExpressionArgument Argument) 12 | { 13 | /// 14 | public override int GetHashCode() => Argument.GetHashCode(); 15 | 16 | public bool Equals(WhenStatementsDatum? other) => other?.Argument.Equals(Argument) ?? false; 17 | } 18 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Runtime.CompilerServices; 6 | 7 | [assembly: InternalsVisibleTo("ReactiveMarbles.PropertyChanged.SourceGenerator.Sample")] 8 | [assembly: InternalsVisibleTo("ReactiveMarbles.PropertyChanged.SourceGenerator.Benchmarks")] 9 | [assembly: InternalsVisibleTo("ReactiveMarbles.PropertyChanged.SourceGenerator.Tests")] 10 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator/ReactiveMarbles - Backup.PropertyChanged.SourceGenerator.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | true 6 | false 7 | 8 | true 9 | Produces DI registration for both property and constructor injection using the Splat locators. 10 | $(TargetsForTfmSpecificContentInPackage);PackBuildOutputs 11 | $(NoWarn);AD0001 12 | preview 13 | true 14 | enable 15 | full 16 | True 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | TextTemplatingFileGenerator 45 | Constants.WhenChanged.cs 46 | 47 | 48 | TextTemplatingFileGenerator 49 | Constants.WhenChanging.cs 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | True 60 | True 61 | Constants.WhenChanged.tt 62 | 63 | 64 | True 65 | True 66 | Constants.WhenChanging.tt 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator/ReactiveMarbles.PropertyChanged.SourceGenerator.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | true 6 | false 7 | 8 | true 9 | Produces DI registration for both property and constructor injection using the Splat locators. 10 | $(TargetsForTfmSpecificContentInPackage);PackBuildOutputs 11 | $(NoWarn);AD0001 12 | preview 13 | true 14 | enable 15 | full 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | TextTemplatingFileGenerator 28 | Constants.When.cs 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | True 39 | True 40 | Constants.When.tt 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator/ReactiveMarbles.PropertyChanged.SourceGenerator.csproj.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True 3 | True 4 | True 5 | True 6 | True 7 | True -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator/SyntaxReceiver.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Collections.Generic; 6 | 7 | using Microsoft.CodeAnalysis; 8 | using Microsoft.CodeAnalysis.CSharp.Syntax; 9 | 10 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator; 11 | 12 | internal class SyntaxReceiver : ISyntaxReceiver 13 | { 14 | public List WhenChanged { get; } = new(); 15 | 16 | public List WhenChanging { get; } = new(); 17 | 18 | public List BindOneWay { get; } = new(); 19 | 20 | public List BindTwoWay { get; } = new(); 21 | 22 | /// 23 | public void OnVisitSyntaxNode(SyntaxNode syntaxNode) 24 | { 25 | if (syntaxNode is not InvocationExpressionSyntax invocationExpression) 26 | { 27 | return; 28 | } 29 | 30 | var methodName = (invocationExpression.Expression as MemberAccessExpressionSyntax)?.Name.ToString() ?? 31 | (invocationExpression.Expression as MemberBindingExpressionSyntax)?.Name.ToString(); 32 | 33 | if (string.Equals(methodName, nameof(WhenChanged))) 34 | { 35 | WhenChanged.Add(invocationExpression); 36 | } 37 | 38 | if (string.Equals(methodName, nameof(WhenChanging))) 39 | { 40 | WhenChanging.Add(invocationExpression); 41 | } 42 | 43 | if (string.Equals(methodName, nameof(BindOneWay))) 44 | { 45 | BindOneWay.Add(invocationExpression); 46 | } 47 | 48 | if (string.Equals(methodName, nameof(BindTwoWay))) 49 | { 50 | BindTwoWay.Add(invocationExpression); 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.SourceGenerator/TypeSymbolComparer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | 8 | using Microsoft.CodeAnalysis; 9 | 10 | namespace ReactiveMarbles.PropertyChanged.SourceGenerator; 11 | 12 | internal class TypeSymbolComparer : IComparer, IEqualityComparer 13 | { 14 | public static TypeSymbolComparer Default { get; } = new(); 15 | 16 | public int Compare(ITypeSymbol x, ITypeSymbol y) 17 | { 18 | if (ReferenceEquals(x, y)) 19 | { 20 | return 0; 21 | } 22 | 23 | if (x is INamedTypeSymbol xNamed && y is INamedTypeSymbol yNamed) 24 | { 25 | return string.CompareOrdinal(xNamed.ToDisplayString(), yNamed.ToDisplayString()); 26 | } 27 | 28 | return string.CompareOrdinal(x.Name, y.Name); 29 | } 30 | 31 | public bool Equals(ITypeSymbol? x, ITypeSymbol? y) 32 | { 33 | if (x is null && y is null) 34 | { 35 | return true; 36 | } 37 | 38 | if (x is null || y is null) 39 | { 40 | return false; 41 | } 42 | 43 | return x.ToDisplayString().Equals(y.ToDisplayString(), StringComparison.InvariantCulture); 44 | } 45 | 46 | public int GetHashCode(ITypeSymbol obj) => obj.ToDisplayString().GetHashCode(); 47 | } 48 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.Tests/BindTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using ReactiveMarbles.PropertyChanged.Tests.Moqs; 6 | 7 | using Xunit; 8 | 9 | namespace ReactiveMarbles.PropertyChanged.Tests; 10 | 11 | /// 12 | /// Tests to make sure that Bind works. 13 | /// 14 | public class BindTests 15 | { 16 | /// 17 | /// Tests one way binding. 18 | /// 19 | [Fact] 20 | public void OneWayBindTest() 21 | { 22 | var a = new A(); 23 | var b = new B(); 24 | a.B = b; 25 | var c = new C(); 26 | b.C = c; 27 | 28 | var bindToC = new C(); 29 | c.Test = "start value"; 30 | 31 | a.BindOneWay(bindToC, x => x.B.C.Test, x => x.Test); 32 | 33 | Assert.Equal("start value", bindToC.Test); 34 | 35 | c.Test = "Hello World"; 36 | 37 | Assert.Equal("Hello World", bindToC.Test); 38 | 39 | a.B = new() { C = new() }; 40 | 41 | Assert.Null(bindToC.Test); 42 | } 43 | 44 | /// 45 | /// Tests one way binding with a converter. 46 | /// 47 | [Fact] 48 | public void OneWayBindWithConverterTest() 49 | { 50 | var a = new A(); 51 | var b = new B(); 52 | a.B = b; 53 | var c = new C(); 54 | b.C = c; 55 | 56 | var bindToC = new C(); 57 | c.Test = "start value"; 58 | 59 | a.BindOneWay(bindToC, x => x.B.C, x => x.Test, value => value.Test); 60 | 61 | Assert.Equal("start value", bindToC.Test); 62 | 63 | b.C = new(); 64 | 65 | Assert.Null(bindToC.Test); 66 | 67 | b.C = new() { Test = "blah" }; 68 | 69 | Assert.Equal("blah", bindToC.Test); 70 | } 71 | 72 | /// 73 | /// Tests two way binding. 74 | /// 75 | [Fact] 76 | public void TwoWayBindTest() 77 | { 78 | var a = new A(); 79 | var b = new B(); 80 | a.B = b; 81 | var c = new C 82 | { 83 | Test = "Host Value", 84 | }; 85 | b.C = c; 86 | 87 | var bindToC = new C 88 | { 89 | Test = "Target value" 90 | }; 91 | 92 | a.BindTwoWay(bindToC, x => x.B.C.Test, x => x.Test); 93 | 94 | Assert.Equal("Host Value", bindToC.Test); 95 | 96 | bindToC.Test = "Test2"; 97 | 98 | Assert.Equal("Test2", c.Test); 99 | 100 | a.B = new() 101 | { 102 | C = new() 103 | { 104 | Test = "Test3", 105 | }, 106 | }; 107 | Assert.Equal("Test3", bindToC.Test); 108 | } 109 | 110 | /// 111 | /// Tests two way binding with a converter. 112 | /// 113 | [Fact] 114 | public void TwoWayBindTestConverter() 115 | { 116 | var a = new A(); 117 | var b = new B(); 118 | a.B = b; 119 | var c = new C 120 | { 121 | Test = "Host Value" 122 | }; 123 | b.C = c; 124 | 125 | var bindToC = new C 126 | { 127 | Test = "Target value" 128 | }; 129 | 130 | a.BindTwoWay(bindToC, x => x.B.C, x => x.Test, x => x.Test, y => new() { Test = y }); 131 | 132 | Assert.Equal("Host Value", bindToC.Test); 133 | 134 | bindToC.Test = "Test2"; 135 | 136 | Assert.Equal("Test2", a.B.C.Test); 137 | } 138 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.Tests/Moqs/A.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace ReactiveMarbles.PropertyChanged.Tests.Moqs; 6 | 7 | internal class A : BaseTestClass 8 | { 9 | private B _b; 10 | 11 | public B B 12 | { 13 | get => _b; 14 | set => RaiseAndSetIfChanged(ref _b, value); 15 | } 16 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.Tests/Moqs/B.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace ReactiveMarbles.PropertyChanged.Tests.Moqs; 6 | 7 | internal class B : BaseTestClass 8 | { 9 | private C _c; 10 | 11 | public C C 12 | { 13 | get => _c; 14 | set => RaiseAndSetIfChanged(ref _c, value); 15 | } 16 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.Tests/Moqs/BaseTestClass.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Collections.Generic; 6 | using System.ComponentModel; 7 | using System.Runtime.CompilerServices; 8 | 9 | namespace ReactiveMarbles.PropertyChanged.Tests.Moqs; 10 | 11 | internal abstract class BaseTestClass : INotifyPropertyChanged 12 | { 13 | public event PropertyChangedEventHandler PropertyChanged; 14 | 15 | protected void RaiseAndSetIfChanged(ref T fieldValue, T value, [CallerMemberName] string propertyName = null) 16 | { 17 | if (EqualityComparer.Default.Equals(fieldValue, value)) 18 | { 19 | return; 20 | } 21 | 22 | fieldValue = value; 23 | OnPropertyChanged(propertyName); 24 | } 25 | 26 | protected virtual void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new(propertyName)); 27 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.Tests/Moqs/C.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace ReactiveMarbles.PropertyChanged.Tests.Moqs; 6 | 7 | internal class C : BaseTestClass 8 | { 9 | private string _test; 10 | 11 | public string Test 12 | { 13 | get => _test; 14 | set => RaiseAndSetIfChanged(ref _test, value); 15 | } 16 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.Tests/ReactiveMarbles.PropertyChanged.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | false 7 | $(NoWarn);SA1600 8 | preview 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged.Tests/WhenChangedTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | 7 | using ReactiveMarbles.PropertyChanged.Tests.Moqs; 8 | 9 | using Xunit; 10 | 11 | namespace ReactiveMarbles.PropertyChanged.Tests; 12 | 13 | /// 14 | /// Whens the WhenChanged. 15 | /// 16 | public class WhenChangedTests 17 | { 18 | /// 19 | /// Checks to make sure that nested property value changes work. 20 | /// 21 | [Fact] 22 | public void NestedPropertyValueChangedWork() 23 | { 24 | var a = new A(); 25 | var b = new B(); 26 | a.B = b; 27 | var c = new C(); 28 | b.C = c; 29 | 30 | var testValue = "ignore"; 31 | a.WhenChanged(x => x.B.C.Test).Subscribe(x => testValue = x); 32 | Assert.Null(testValue); 33 | 34 | c.Test = "Hello World"; 35 | 36 | Assert.Equal("Hello World", testValue); 37 | 38 | a.B = new() { C = new() }; 39 | 40 | Assert.Null(testValue); 41 | } 42 | 43 | /// 44 | /// Checks to make sure that property value changes work. 45 | /// 46 | [Fact] 47 | public void PropertyValueChangedWork() 48 | { 49 | var c = new C(); 50 | var testValue = "ignore"; 51 | c.WhenChanged(x => x.Test).Subscribe(x => testValue = x); 52 | 53 | Assert.Null(testValue); 54 | c.Test = "test"; 55 | Assert.Equal("test", testValue); 56 | 57 | c.Test = null; 58 | Assert.Null(testValue); 59 | } 60 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged/ExpressionExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Collections.Concurrent; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Linq.Expressions; 10 | 11 | namespace ReactiveMarbles.PropertyChanged; 12 | 13 | internal static class ExpressionExtensions 14 | { 15 | private static readonly ConcurrentDictionary _actionCache = 16 | new(); 17 | 18 | internal static List GetExpressionChain(this Expression expression) 19 | { 20 | var expressions = new List(16); 21 | 22 | var node = expression; 23 | 24 | while (node.NodeType != ExpressionType.Parameter) 25 | { 26 | switch (node.NodeType) 27 | { 28 | case ExpressionType.MemberAccess: 29 | var memberExpression = (MemberExpression)node; 30 | expressions.Add(memberExpression); 31 | node = memberExpression.Expression; 32 | break; 33 | default: 34 | throw new NotSupportedException($"Unsupported expression type: '{node.NodeType.ToString()}'"); 35 | } 36 | } 37 | 38 | expressions.Reverse(); 39 | 40 | return expressions; 41 | } 42 | 43 | internal static Action GetSetter(this Expression> expression) 44 | => (Action)_actionCache.GetOrAdd( 45 | $"{typeof(T).FullName}|{typeof(TProperty).FullName}|{expression}", 46 | _ => 47 | { 48 | var instanceParameter = expression.Parameters.Single(); 49 | var valueParameter = Expression.Parameter(typeof(TProperty), "value"); 50 | 51 | return Expression.Lambda>( 52 | Expression.Assign(expression.Body, valueParameter), 53 | instanceParameter, 54 | valueParameter) 55 | .Compile(); 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged/GetMemberFuncCache.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Collections.Concurrent; 7 | using System.Linq.Expressions; 8 | using System.Reflection; 9 | 10 | namespace ReactiveMarbles.PropertyChanged; 11 | 12 | internal static class GetMemberFuncCache 13 | { 14 | #if !UIKIT 15 | private static readonly ConcurrentDictionary> Cache = new(new MemberFuncCacheKeyComparer()); 16 | #endif 17 | 18 | public static Func GetCache(MemberInfo memberInfo) => 19 | #if UIKIT 20 | memberInfo switch 21 | { 22 | PropertyInfo propertyInfo => input => (TReturn)propertyInfo.GetValue(input), 23 | FieldInfo fieldInfo => input => (TReturn)fieldInfo.GetValue(input), 24 | _ => throw new ArgumentException($"Cannot handle member {memberInfo.Name}", nameof(memberInfo)), 25 | }; 26 | #else 27 | Cache.GetOrAdd(memberInfo, static memberInfo => 28 | { 29 | var instance = Expression.Parameter(typeof(TFrom), "instance"); 30 | 31 | var castInstance = Expression.Convert(instance, memberInfo.DeclaringType); 32 | 33 | Expression body = memberInfo switch 34 | { 35 | PropertyInfo propertyInfo => Expression.Call(castInstance, propertyInfo.GetGetMethod()), 36 | FieldInfo fieldInfo => Expression.Field(castInstance, fieldInfo), 37 | _ => throw new ArgumentException($"Cannot handle member {memberInfo.Name}", nameof(memberInfo)), 38 | }; 39 | 40 | var parameters = new[] { instance }; 41 | 42 | var lambdaExpression = Expression.Lambda>(body, parameters); 43 | 44 | return lambdaExpression.Compile(); 45 | }); 46 | #endif 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged/MemberFuncCacheKeyComparer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Collections.Generic; 6 | using System.Reflection; 7 | 8 | namespace ReactiveMarbles.PropertyChanged; 9 | #if !UIKIT 10 | internal sealed class MemberFuncCacheKeyComparer : IEqualityComparer 11 | { 12 | public bool Equals(MemberInfo x, MemberInfo y) => (x.DeclaringType, x.Name) == (y.DeclaringType, y.Name); 13 | 14 | public int GetHashCode(MemberInfo obj) => (obj.DeclaringType, obj.Name).GetHashCode(); 15 | } 16 | #endif 17 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged/NotifyPropertiesChangeExtensions.tt: -------------------------------------------------------------------------------- 1 | <#@ template debug="false" hostspecific="false" language="C#" #> 2 | <#@ assembly name="System.Core.dll" #> 3 | <#@ assembly name="System.Collections.dll" #> 4 | <#@ import namespace="System.Linq" #> 5 | <#@ output extension=".cs" #> 6 | // Copyright (c) 2019-2021 ReactiveUI Association Incorporated. All rights reserved. 7 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 8 | // See the LICENSE file in the project root for full license information. 9 | 10 | using System; 11 | using System.ComponentModel; 12 | using System.Linq.Expressions; 13 | using System.Reactive.Linq; 14 | 15 | <# 16 | const int maxFuncLength = 12; 17 | #> 18 | namespace ReactiveMarbles.PropertyChanged 19 | { 20 | /// 21 | /// Provides extension methods for the notify property changed extensions. 22 | /// 23 | public static class NotifyPropertiesChangeExtensions 24 | { 25 | <# 26 | for(var length = 2; length <= maxFuncLength; length++) 27 | { 28 | var templateParams = Enumerable.Range(1, length).Select(x => "TTempReturn" + x).ToList(); 29 | var expressionParameterNames = Enumerable.Range(1, length).Select(x => "propertyExpression" + x).ToList(); 30 | var observableNames = Enumerable.Range(1, length).Select(x => "obs" + x).ToList();#> 31 | /// 32 | /// Notifies when the specified property changes. 33 | /// 34 | /// The object to monitor. 35 | <# 36 | for (var i = 0; i < expressionParameterNames.Count; ++i) 37 | { 38 | var expressionParameterName = expressionParameterNames[i]; 39 | var expressionParameterNumber = i + 1;#> 40 | /// A expression to the value<#= expressionParameterNumber #>. 41 | <# 42 | } 43 | #> 44 | /// Parameter which converts into the end value. 45 | /// The type of initial object. 46 | <# 47 | for (var i = 0; i < templateParams.Count; ++i) 48 | { 49 | var templateParam = templateParams[i]; 50 | var templateParamNumber = i + 1;#> 51 | /// The return type of the value<#= templateParamNumber #>. 52 | <# 53 | } 54 | #> 55 | /// The return value of the observable. Generated from the conversion func. 56 | /// An observable that signals when the properties specified in the expressions have changed. 57 | /// Either the property expression or the object to monitor is null. 58 | /// If there is an issue with the property expression. 59 | public static IObservable WhenChanged, TReturn>( 60 | this TObj objectToMonitor, 61 | <# 62 | for (var i = 0; i < templateParams.Count; ++i) 63 | { 64 | var templateParam = templateParams[i]; 65 | var templateParamNumber = i + 1;#> 66 | Expression>> propertyExpression<#= templateParamNumber #>, 67 | <# 68 | } 69 | #> 70 | Func<<#= string.Join(", ", templateParams) #>, TReturn> conversionFunc) 71 | where TObj : class, INotifyPropertyChanged 72 | {<# 73 | for (var i = 0; i < templateParams.Count; ++i) 74 | { 75 | var templateParamNumber = i + 1;#> 76 | 77 | var obs<#= templateParamNumber #> = objectToMonitor.WhenChanged(propertyExpression<#= templateParamNumber #>); 78 | <# }#> 79 | 80 | return obs1.CombineLatest(<#= string.Join(", ", observableNames.Skip(1)) #>, conversionFunc); 81 | } 82 | <# 83 | if (length != maxFuncLength) WriteLine(string.Empty); 84 | }#> 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.PropertyChanged/ReactiveMarbles.PropertyChanged.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;Xamarin.iOS10;Xamarin.TVOS10;Xamarin.WatchOS10;net6.0 5 | $(TargetFrameworks);net462 6 | preview 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | TextTemplatingFileGenerator 16 | NotifyPropertiesChangeExtensions.cs 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | True 27 | True 28 | NotifyPropertiesChangeExtensions.tt 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/global.json: -------------------------------------------------------------------------------- 1 | { 2 | "msbuild-sdks": { 3 | "MSBuild.Sdk.Extras": "3.0.44" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 3 | "settings": { 4 | "indentation": { 5 | "useTabs": false, 6 | "indentationSize": 4 7 | }, 8 | "documentationRules": { 9 | "documentExposedElements": true, 10 | "documentInternalElements": false, 11 | "documentPrivateElements": false, 12 | "documentInterfaces": true, 13 | "documentPrivateFields": false, 14 | "documentationCulture": "en-US", 15 | "companyName": "ReactiveUI Association Incorporated", 16 | "copyrightText": "Copyright (c) 2019-2021 {companyName}. All rights reserved.\n{companyName} licenses this file to you under the {licenseName} license.\nSee the {licenseFile} file in the project root for full license information.", 17 | "variables": { 18 | "licenseName": "MIT", 19 | "licenseFile": "LICENSE" 20 | }, 21 | "xmlHeader": false 22 | }, 23 | "layoutRules": { 24 | "newlineAtEndOfFile": "allow", 25 | "allowConsecutiveUsings": true 26 | }, 27 | "maintainabilityRules": { 28 | "topLevelTypes": [ 29 | "class", 30 | "interface", 31 | "struct", 32 | "enum", 33 | "delegate" 34 | ] 35 | }, 36 | "orderingRules": { 37 | "usingDirectivesPlacement": "outsideNamespace", 38 | "systemUsingDirectivesFirst": true 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "publicReleaseRefSpec": [ 4 | "^refs/heads/master$", 5 | "^refs/heads/main$" 6 | ], 7 | "nugetPackageVersion":{ 8 | "semVer": 2 9 | }, 10 | "cloudBuild": { 11 | "setVersionVariables": true, 12 | "buildNumber": { 13 | "enabled": false 14 | } 15 | } 16 | } 17 | --------------------------------------------------------------------------------