├── .editorconfig ├── .github ├── .commitsar.yml ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── conventional-commits.yml ├── .gitignore ├── .mergify.yml ├── BREAKING_CHANGES.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Directory.Build.targets ├── DynamicMvvm.sln ├── LICENSE ├── README.md ├── build ├── azure-pipelines.yml ├── gitversion.yml ├── stage-build.yml └── stage-release.yml ├── nuget.config └── src ├── DynamicMvvm.Abstractions ├── AssemblyInfo.cs ├── Command │ ├── DecoratorCommandStrategy.cs │ ├── IDynamicCommand.Extensions.cs │ ├── IDynamicCommand.cs │ ├── IDynamicCommand.md │ ├── IDynamicCommandBuilder.Extensions.cs │ ├── IDynamicCommandBuilder.cs │ ├── IDynamicCommandBuilderFactory.cs │ └── IDynamicCommandStrategy.cs ├── Deactivation │ ├── IDeactivatable.cs │ └── IDeactivatableViewModel.cs ├── DynamicMvvm.Abstractions.csproj ├── LoggerMessages.Abstractions.cs ├── MvvmConfiguration.cs ├── Property │ ├── IDynamicProperty.Extensions.cs │ ├── IDynamicProperty.cs │ ├── IDynamicProperty.md │ └── IDynamicPropertyFactory.cs └── ViewModel │ ├── IDispatcher.cs │ ├── IDispatcherFactory.cs │ ├── IViewModel.Extensions.Children.cs │ ├── IViewModel.Extensions.Commands.cs │ ├── IViewModel.Extensions.Properties.cs │ ├── IViewModel.Extensions.Services.cs │ ├── IViewModel.Extensions.cs │ ├── IViewModel.cs │ └── IViewModel.md ├── DynamicMvvm.Benchmarks ├── DynamicMvvm.Benchmarks.csproj ├── IViewModel.Extensions.Benchmark.cs ├── Program.cs ├── TestViewModelBase.cs ├── ViewModel.cs └── ViewModelBase.Benchmark.cs ├── DynamicMvvm.CollectionTracking ├── DynamicMvvm.CollectionTracking.csproj ├── IViewModel.Extensions.CollectionTracking.cs └── ObservableCollectionFromObservableAdapter.cs ├── DynamicMvvm.FluentValidation ├── DynamicMvvm.FluentValidation.csproj ├── IViewModel.Extensions.Validation.cs └── LoggerMessages.FluentValidation.cs ├── DynamicMvvm.Reactive ├── Command │ └── IDynamicCommandExtensions.cs ├── Deactivation │ ├── DeactivatableObservable.cs │ └── IObservable.Extensions.cs ├── DynamicMvvm.Reactive.csproj ├── LoggerMessages.Reactive.cs └── Property │ └── IDynamicPropertyExtensions.cs ├── DynamicMvvm.Shared ├── DynamicMvvm.Shared.projitems ├── DynamicMvvm.Shared.shproj └── PreserveAttribute.cs ├── DynamicMvvm.Tests ├── CollectionTracking │ └── ObservableCollectionFromObservableAdapterTests.cs ├── Command │ ├── DynamicCommandBuilderExtensionsTests.cs │ ├── DynamicCommandBuilderFactoryTests.cs │ ├── DynamicCommandReactiveTests.cs │ ├── DynamicCommandTests.cs │ └── Strategies │ │ ├── ActionCommandStrategyTests.cs │ │ ├── BackgroundCommandStrategyTests.cs │ │ ├── CanExecuteCommandStrategyTests.cs │ │ ├── CancelPreviousCommandStrategyTests.cs │ │ ├── DisableWhileExecutingCommandStrategyTests.cs │ │ ├── ErrorHandlerCommandStrategyTests.cs │ │ ├── LockCommandStrategyTests.cs │ │ ├── RaiseCanExecuteOnDispatcherCommandStrategyTests.cs │ │ ├── SkipWhileExecutingCommandStrategyTests.cs │ │ └── TaskCommandStrategyTests.cs ├── DynamicMvvm.Tests.csproj ├── Helpers │ ├── TestCommandStrategy.cs │ ├── TestDisposable.cs │ ├── TestEntity.cs │ ├── TestSubscriber.cs │ ├── TestViewModel.cs │ └── TestViewModelView.cs ├── Integration │ ├── DeactivationIntegrationTests.cs │ └── IntegrationTests.cs ├── Property │ ├── DynamicPropertyFactoryTests.cs │ ├── DynamicPropertyFromObservableTests.cs │ ├── DynamicPropertyFromTaskTests.cs │ ├── DynamicPropertyReactiveTests.cs │ ├── DynamicPropertyTests.T.cs │ ├── DynamicPropertyTests.cs │ └── ValueChangedOnBackgroundTaskDynamicPropertyTests.cs ├── Reactive │ ├── DeactivatableObservableTests.cs │ └── DynamicPropertyExtensionsTests.cs └── ViewModel │ ├── DeactivatableViewModelBaseTests.cs │ ├── ViewModelBaseChildrenTests.cs │ ├── ViewModelBaseCommandsTest.cs │ ├── ViewModelBaseDisposableTests.cs │ ├── ViewModelBaseErrorsTests.cs │ ├── ViewModelBasePropertiesTests.cs │ ├── ViewModelBasePropertyChangedTests.cs │ ├── ViewModelBaseServicesTests.cs │ └── ViewModelBaseTests.cs ├── DynamicMvvm.Uno.WinUI ├── Dispatchers │ ├── BatchingDispatcherQueueDispatcher.cs │ ├── DispatcherFactory.cs │ └── DispatcherQueueDispatcher.cs ├── DynamicMvvm.Uno.WinUI.csproj ├── Extensions │ └── DispatcherQueueExtensions.cs ├── LoggerMessages.Uno.WinUI.cs ├── ViewModel │ └── IViewModel.Extensions.Uno.cs └── winappsdk-workaround.targets └── DynamicMvvm ├── Command ├── DynamicCommand.cs ├── DynamicCommandBuilder.cs ├── DynamicCommandBuilderFactory.cs └── Strategies │ ├── ActionCommandStrategy.T.cs │ ├── ActionCommandStrategy.cs │ ├── BackgroundCommandStrategy.cs │ ├── CanExecuteCommandStrategy.cs │ ├── CancelPreviousCommandStrategy.cs │ ├── DisableWhileExecutingCommandStrategy.cs │ ├── ErrorHandlerCommandStrategy.cs │ ├── LockCommandStrategy.cs │ ├── LoggerCommandStrategy.cs │ ├── RaiseCanExecuteOnDispatcherCommandStrategy.cs │ ├── SkipWhileExecutingCommandStrategy.cs │ ├── TaskCommandStrategy.T.cs │ └── TaskCommandStrategy.cs ├── Deactivation ├── DeactivatableDynamicPropertyFromObservable.cs ├── DeactivatableViewModelBase.cs └── IDeactivatableViewModel.Extensions.cs ├── DynamicMvvm.csproj ├── LoggerMessages.cs ├── PreserveAttribute.cs ├── Property ├── DynamicProperty.T.cs ├── DynamicProperty.cs ├── DynamicPropertyFactory.cs ├── DynamicPropertyFromObservable.cs ├── DynamicPropertyFromTask.cs └── ValueChangedOnBackgroundTask │ ├── ValueChangedOnBackgroundTaskDynamicProperty.cs │ ├── ValueChangedOnBackgroundTaskDynamicPropertyFactory.cs │ ├── ValueChangedOnBackgroundTaskDynamicPropertyFromObservable.cs │ └── ValueChangedOnBackgroundTaskDynamicPropertyFromTask.cs └── ViewModel ├── ViewModelBase.Dispatcher.cs ├── ViewModelBase.Disposable.cs ├── ViewModelBase.Errors.cs ├── ViewModelBase.PropertyChanged.cs └── ViewModelBase.cs /.github/.commitsar.yml: -------------------------------------------------------------------------------- 1 | commits: 2 | strict: false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Expected Behavior 2 | 3 | 4 | ## Actual Behavior 5 | 6 | 7 | ## Steps to Reproduce the Problem 8 | 9 | 1. 10 | 2. 11 | 3. 12 | 13 | ## Specifications 14 | 15 | - Version: 16 | - Platform: 17 | - Subsystem: -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | GitHub Issue: # 2 | 4 | 5 | ## Proposed Changes 6 | 7 | 8 | - [ ] Bug fix 9 | - [ ] Feature 10 | - [ ] Code style update (formatting) 11 | - [ ] Refactoring (no functional changes, no api changes) 12 | - [ ] Build or CI related changes 13 | - [ ] Documentation content changes 14 | - [ ] Other, please describe: 15 | 16 | ## What is the current behavior? 17 | 19 | 20 | ## What is the new behavior? 21 | 22 | 23 | ## Impact on version 24 | 25 | 26 | - [ ] **Major** (Public API was modified.) 27 | - Public constructs (class, struct, delegate, enum, etc.) were removed or renamed. 28 | - Public members were removed or renamed. 29 | - Public method signatures were changed or renamed. 30 | - [ ] **Minor** (Public API was extended.) 31 | - Public constructs, members, or overloads were added. 32 | - [ ] **Patch** (Public API was unchanged.) 33 | - A bug in behavior was fixed. 34 | - Documentation was changed. 35 | - [ ] **None** (The library is unchanged.) 36 | - Only code under the `build` folder was changed. 37 | - Only code under the `.github` folder was changed. 38 | - Only code in the Benchmarks project was changed. 39 | 40 | ## Checklist 41 | 42 | Please check that your PR fulfills the following requirements: 43 | 44 | - [ ] Documentation has been added/updated. 45 | - [ ] Automated Unit / Integration tests for the changes have been added/updated. 46 | - [ ] Updated [BREAKING_CHANGES.md](../BREAKING_CHANGES.md) (if you introduced a breaking change). 47 | - [ ] Your conventional commits are aligned with the **Impact on version** section. 48 | 49 | 51 | 52 | ## Other information 53 | 54 | 55 | -------------------------------------------------------------------------------- /.github/workflows/conventional-commits.yml: -------------------------------------------------------------------------------- 1 | name: Conventional Commits 2 | 3 | on: [pull_request] 4 | 5 | env: 6 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 7 | 8 | jobs: 9 | validate-commits: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check out code into the Go module directory 13 | uses: actions/checkout@v1 14 | - name: Commitsar check 15 | uses: docker://aevea/commitsar 16 | env: 17 | COMMITSAR_CONFIG_PATH : ./.github 18 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | queue_rules: 2 | - name: default 3 | conditions: 4 | # Conditions to get out of the queue (= merged) 5 | - check-success=nventive.Chinook.DynamicMvvm 6 | 7 | pull_request_rules: 8 | 9 | - name: automatic strict merge when CI passes, has 2 reviews, no requests for change and is labeled 'ready-to-merge' unless labelled 'do-not-merge/breaking-change' or 'do-not-merge/work-in-progress' 10 | conditions: 11 | # Only pull-requests sent to the main branch 12 | - base=main 13 | 14 | # All Azure builds should be green: 15 | - status-success=nventive.Chinook.DynamicMvvm 16 | 17 | # CLA check must pass: 18 | #- "status-success=license/cla" 19 | 20 | # Note that this only matches people with write / admin access to the repo, 21 | # see 22 | - "#approved-reviews-by>=2" 23 | - "#changes-requested-reviews-by=0" 24 | 25 | # Pull-request must be labeled with: 26 | - label=ready-to-merge 27 | 28 | # Do not automatically merge pull-requests that are labelled as do-not-merge 29 | # see 30 | - label!=do-not-merge/breaking-change 31 | - label!=do-not-merge/work-in-progress 32 | 33 | # Note: mergify cannot break branch protection rules 34 | actions: 35 | queue: 36 | method: merge 37 | name: default 38 | 39 | - name: automatic merge for allcontributors pull requests 40 | conditions: 41 | - author=allcontributors[bot] 42 | actions: 43 | merge: 44 | method: merge 45 | -------------------------------------------------------------------------------- /BREAKING_CHANGES.md: -------------------------------------------------------------------------------- 1 | # Breaking Changes 2 | 3 | ## 3.0.0 4 | - Added support for .NET 8. 5 | - Removed support for .NET 7. 6 | 7 | ## 2.0.0 8 | - Added support for .NET 7. 9 | - Updated Uno.WinUI to 5.0.19. 10 | - Updated Windows SDK version from 18362 to 19041. 11 | - Removed support for Xamarin. 12 | - Removed support for .NET 6. 13 | - Removed support for NetStandard2.0 in DynamicMvvm.Uno.WinUI. 14 | 15 | ## 1.4.1 16 | - Dynamic properties no longer throw an `ObjectDisposedException` when we set their `Value` while they're disposed. 17 | - We've discovered that this safeguard is not needed and was causing unjustified issues when using dynamic properties in a multi-threaded environment. This is especially true with the _DynamicPropertyFromObservable_ variant, which can easily be disposed from a different thread than the one the observable source uses. 18 | - This change renders obsolete the `throwOnDisposed` parameter used in several constructors of `IDynamicProperty` and `IDynamicPropertyFactory` implementations. 19 | Those overloads are still present in the library, but they are marked as obsolete and will be removed in a future version. 20 | - You can still observe the events where a dynamic property is set while it's disposed by using logs. The event id is 32, the log level is `Debug`, and the message template is `"Skipped value setter on the property '{PropertyName}' because it's disposed."` 21 | 22 | This breaking changes doesn't change the API definition. 23 | 24 | ## 1.3.0 25 | - The NuGet reference to `Microsoft.Extensions.Logging.Abstractions` now requires version 6.0.0 and up. 26 | 27 | This breaking change doesn't change the API definition. 28 | 29 | ## 0.11.0 30 | - `DecoratorCommandStrategy` not longer exists. Use `DelegatingCommandStrategy` instead. 31 | 32 | ## 0.10.0 33 | - Replaced `IViewModelView` with `IDispatcher`. 34 | - Replaced `IViewModel.View` with `IViewModel.Dispatcher` 35 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and 9 | expression, level of experience, education, socio-economic status, nationality, 10 | personal appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or reject 41 | comments, commits, code, wiki edits, issues, and other contributions that are 42 | not aligned to this Code of Conduct, or to ban temporarily or permanently any 43 | contributor for other behaviors that they deem inappropriate, threatening, 44 | offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at info@nventive.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an 62 | incident. Further details of specific enforcement policies may be posted 63 | separately. 64 | 65 | Project maintainers who do not follow or enforce the Code of Conduct in good 66 | faith may face temporary or permanent repercussions as determined by other 67 | members of the project's leadership. 68 | 69 | ## Attribution 70 | 71 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 72 | version 1.4, available at 73 | https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 74 | 75 | [homepage]: https://www.contributor-covenant.org 76 | 77 | For answers to common questions about this code of conduct, see 78 | https://www.contributor-covenant.org/faq 79 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches, contributions and suggestions to this project. 4 | Here are a few small guidelines you need to follow. 5 | 6 | ## Code of conduct 7 | 8 | To better foster an open, innovative and inclusive community please refer to our 9 | [Code of Conduct](CODE_OF_CONDUCT.md) when contributing. 10 | 11 | ### Report a bug 12 | 13 | If you think you've found a bug, please log a new issue in the [GitHub issue 14 | tracker. When filing issues, please use our [issue 15 | template](.github/ISSUE_TEMPLATE.md). The best way to get your bug fixed is to 16 | be as detailed as you can be about the problem. Providing a minimal project with 17 | steps to reproduce the problem is ideal. Here are questions you can answer 18 | before you file a bug to make sure you're not missing any important information. 19 | 20 | 1. Did you read the documentation? 21 | 2. Did you include the snippet of broken code in the issue? 22 | 3. What are the *EXACT* steps to reproduce this problem? 23 | 4. What specific version or build are you using? 24 | 5. What operating system are you using? 25 | 26 | GitHub supports 27 | [markdown](https://help.github.com/articles/github-flavored-markdown/), so when 28 | filing bugs make sure you check the formatting before clicking submit. 29 | 30 | ### Make a suggestion 31 | 32 | If you have an idea for a new feature or enhancement let us know by filing an 33 | issue. To help us understand and prioritize your idea please provide as much 34 | detail about your scenario and why the feature or enhancement would be useful. 35 | 36 | ## Contributing code and content 37 | 38 | This is an open source project and we welcome code and content contributions 39 | from the community. 40 | 41 | **Identifying the scale** 42 | 43 | If you would like to contribute to this project, first identify the scale of 44 | what you would like to contribute. If it is small (grammar/spelling or a bug 45 | fix) feel free to start working on a fix. 46 | 47 | If you are submitting a feature or substantial code contribution, please discuss 48 | it with the team. You might also read these two blogs posts on contributing 49 | code: [Open Source Contribution 50 | Etiquette](http://tirania.org/blog/archive/2010/Dec-31.html) by Miguel de Icaza 51 | and [Don't "Push" Your Pull 52 | Requests](https://www.igvita.com/2011/12/19/dont-push-your-pull-requests/) by 53 | Ilya Grigorik. Note that all code submissions will be rigorously reviewed and 54 | tested by the project team, and only those that meet an extremely high bar for 55 | both quality and design/roadmap appropriateness will be merged into the source. 56 | 57 | **Obtaining the source code** 58 | 59 | If you are an outside contributor, please fork the repository to your account. 60 | See the GitHub documentation for [forking a 61 | repo](https://help.github.com/articles/fork-a-repo/) if you have any questions 62 | about this. 63 | 64 | **Submitting a pull request** 65 | 66 | If you don't know what a pull request is read this article: 67 | https://help.github.com/articles/using-pull-requests. Make sure the repository 68 | can build and all tests pass, as well as follow the current coding guidelines. 69 | When submitting a pull request, please use our [pull request 70 | template](.github/PULL_REQUEST_TEMPLATE.md). 71 | 72 | Pull requests should all be done to the **main** branch. 73 | 74 | --- 75 | 76 | ## Code reviews 77 | 78 | All submissions, including submissions by project members, require review. We 79 | use GitHub pull requests for this purpose. Consult [GitHub 80 | Help](https://help.github.com/articles/about-pull-requests/) for more 81 | information on using pull requests. 82 | 83 | ## Community Guidelines 84 | 85 | This project follows [Google's Open Source Community 86 | Guidelines](https://opensource.google.com/conduct/). -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)')) 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /build/azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | branches: 3 | include: 4 | - main 5 | 6 | variables: 7 | - name: NUGET_VERSION 8 | value: 6.2.0 9 | - name: VSTEST_PLATFORM_VERSION 10 | value: 17.2.5 11 | - name: ArtifactName 12 | value: Packages 13 | - name: SolutionFileName # Example: MyApplication.sln 14 | value: DynamicMvvm.sln 15 | - name: IsReleaseBranch # Should this branch name use the release stage 16 | value: $[or(eq(variables['Build.SourceBranch'], 'refs/heads/main'), startsWith(variables['Build.SourceBranch'], 'refs/heads/feature/'))] 17 | # Pool names 18 | - name: windowsHostedAgentImage 19 | value: 'windows-2022' 20 | 21 | stages: 22 | - stage: Build 23 | jobs: 24 | - job: Windows 25 | strategy: 26 | maxParallel: 3 27 | matrix: 28 | Packages: 29 | ApplicationConfiguration: Release 30 | ApplicationPlatform: NuGet 31 | GeneratePackageOnBuild: true 32 | 33 | pool: 34 | vmImage: $(windowsHostedAgentImage) 35 | 36 | variables: 37 | - name: PackageOutputPath # Path where nuget packages will be copied to. 38 | value: $(Build.ArtifactStagingDirectory) 39 | 40 | workspace: 41 | clean: all # Cleanup the workspaca before starting 42 | 43 | steps: 44 | - template: stage-build.yml 45 | 46 | - stage: Release 47 | # Only release when the build is not for a Pull Request and branch name fits 48 | condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest'), eq(variables['IsReleaseBranch'], 'true')) 49 | jobs: 50 | - job: Publish_NuGet_External 51 | 52 | pool: 53 | vmImage: $(windowsHostedAgentImage) 54 | 55 | workspace: 56 | clean: all # Cleanup the workspaca before starting 57 | 58 | steps: 59 | - template: stage-release.yml 60 | -------------------------------------------------------------------------------- /build/gitversion.yml: -------------------------------------------------------------------------------- 1 | # The version is driven by conventional commits via xxx-version-bump-message. 2 | # Anything merged to main creates a new stable version. 3 | # Only builds from main and feature/* are pushed to nuget.org. 4 | 5 | assembly-versioning-scheme: MajorMinorPatch 6 | mode: MainLine 7 | next-version: '' # Use git tags to set the base version. 8 | continuous-delivery-fallback-tag: "" 9 | commit-message-incrementing: Enabled 10 | major-version-bump-message: "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\\([\\w\\s-]*\\))?(!:|:.*\\n\\n((.+\\n)+\\n)?BREAKING CHANGE:\\s.+)" 11 | minor-version-bump-message: "^(feat)(\\([\\w\\s-]*\\))?:" 12 | patch-version-bump-message: "^(build|chore|docs|fix|perf|refactor|revert|style|test)(\\([\\w\\s-]*\\))?:" 13 | no-bump-message: "^(ci|benchmarks)(\\([\\w\\s-]*\\))?:" # You can use the "ci" or "benchmarks" type to avoid bumping the version when your changes are limited to the [build or .github folders] or limited to benchmark code. 14 | branches: 15 | main: 16 | regex: ^master$|^main$ 17 | tag: '' 18 | dev: 19 | regex: dev/.*?/(.*?) 20 | tag: dev.{BranchName} 21 | source-branches: [main] 22 | feature: 23 | tag: feature.{BranchName} 24 | regex: feature/(.*?) 25 | source-branches: [main] 26 | ignore: 27 | sha: [] -------------------------------------------------------------------------------- /build/stage-release.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - checkout: none 3 | 4 | - task: DownloadBuildArtifacts@0 5 | inputs: 6 | buildType: current 7 | downloadType: single 8 | artifactName: $(ArtifactName) 9 | 10 | - task: NuGetToolInstaller@1 11 | displayName: 'Install NuGet $(NUGET_VERSION)' 12 | inputs: 13 | versionSpec: $(NUGET_VERSION) 14 | checkLatest: false 15 | 16 | - task: NuGetCommand@2 17 | displayName: 'Push to Nuget.org' 18 | inputs: 19 | command: 'push' 20 | packagesToPush: '$(Build.ArtifactStagingDirectory)/$(ArtifactName)/*.nupkg' 21 | nuGetFeedType: 'external' 22 | publishFeedCredentials: 'NuGet.org - nventive' 23 | 24 | - task: PostBuildCleanup@3 25 | displayName: 'Post-Build cleanup : Cleanup files to keep build server clean!' 26 | condition: always() -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Abstractions/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("Chinook.DynamicMvvm")] 4 | [assembly: InternalsVisibleTo("Chinook.DynamicMvvm.FluentValidation")] 5 | [assembly: InternalsVisibleTo("Chinook.DynamicMvvm.Reactive")] 6 | [assembly: InternalsVisibleTo("Chinook.DynamicMvvm.Uno")] 7 | [assembly: InternalsVisibleTo("Chinook.DynamicMvvm.Uno.WinUI")] 8 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Abstractions/Command/DecoratorCommandStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace Chinook.DynamicMvvm 8 | { 9 | /// 10 | /// This delegates the functionalities of to an inner strategy. 11 | /// You may override any member add customization. 12 | /// This class is an homologue to . 13 | /// 14 | public abstract class DelegatingCommandStrategy : IDynamicCommandStrategy 15 | { 16 | /// 17 | /// Initializes a new instance of the class. 18 | /// 19 | public DelegatingCommandStrategy() 20 | { 21 | } 22 | 23 | /// 24 | public virtual IDynamicCommandStrategy InnerStrategy { get; set; } 25 | 26 | /// 27 | public virtual event EventHandler CanExecuteChanged 28 | { 29 | add => InnerStrategy.CanExecuteChanged += value; 30 | remove => InnerStrategy.CanExecuteChanged -= value; 31 | } 32 | 33 | /// 34 | public virtual bool CanExecute(object parameter, IDynamicCommand command) 35 | => InnerStrategy.CanExecute(parameter, command); 36 | 37 | /// 38 | public virtual Task Execute(CancellationToken ct, object parameter, IDynamicCommand command) 39 | => InnerStrategy.Execute(ct, parameter, command); 40 | 41 | /// 42 | public virtual void Dispose() 43 | => InnerStrategy.Dispose(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Abstractions/Command/IDynamicCommand.Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace Chinook.DynamicMvvm 8 | { 9 | /// 10 | /// Extensions on . 11 | /// 12 | public static class IDynamicCommandExtensions 13 | { 14 | /// 15 | /// Executes the specified without a parameter. 16 | /// 17 | /// 18 | /// 19 | public static Task Execute(this IDynamicCommand command) 20 | { 21 | return command.Execute(null); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Abstractions/Command/IDynamicCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.ComponentModel; 5 | using System.Threading.Tasks; 6 | using System.Windows.Input; 7 | 8 | namespace Chinook.DynamicMvvm 9 | { 10 | /// 11 | /// A is a that will notify its subscribers when it is executing. 12 | /// It adds support of async execution using the method. 13 | /// 14 | public interface IDynamicCommand : ICommand, INotifyPropertyChanged, IDisposable 15 | { 16 | /// 17 | /// Gets the name of the command. 18 | /// 19 | string Name { get; } 20 | 21 | /// 22 | /// Gets whether or not the command is currently executing. 23 | /// 24 | bool IsExecuting { get; } 25 | 26 | /// 27 | /// Occurs when changes occur that affect whether or not the command is executing. 28 | /// 29 | event EventHandler IsExecutingChanged; 30 | 31 | /// 32 | /// Task based version of the method. 33 | /// 34 | new Task Execute(object parameter); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Abstractions/Command/IDynamicCommandBuilder.Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace Chinook.DynamicMvvm 7 | { 8 | /// 9 | /// Offers extension methods on . 10 | /// 11 | public static class DynamicCommandBuilderExtensions 12 | { 13 | /// 14 | /// Adds a to the builder. 15 | /// 16 | /// The builder. 17 | /// The strategy to add. 18 | /// When true, the is added at the start of the list, so that it wraps all existing strategies already present in the list. 19 | /// 20 | public static IDynamicCommandBuilder WithStrategy(this IDynamicCommandBuilder builder, DelegatingCommandStrategy strategy, bool wrapExisting = false) 21 | { 22 | if (wrapExisting) 23 | { 24 | builder.Strategies.Insert(0, strategy); 25 | } 26 | else 27 | { 28 | builder.Strategies.Add(strategy); 29 | } 30 | return builder; 31 | } 32 | 33 | /// 34 | /// Removes strategies matching from the builder. 35 | /// 36 | /// Any type implementing . 37 | /// The builder. 38 | /// 39 | public static IDynamicCommandBuilder WithoutStrategy(this IDynamicCommandBuilder builder) 40 | where TStrategy : IDynamicCommandStrategy 41 | { 42 | var itemsToRevome = builder.Strategies.Where(s => s is TStrategy).ToList(); 43 | foreach (var item in itemsToRevome) 44 | { 45 | builder.Strategies.Remove(item); 46 | } 47 | return builder; 48 | } 49 | 50 | /// 51 | /// Removes all strategies from the builder. 52 | /// 53 | /// 54 | /// This can be usefull if you want to completely discard the default configuration. 55 | /// Note that the is not modified, only is cleared. 56 | /// 57 | /// The builder. 58 | /// 59 | public static IDynamicCommandBuilder ClearStrategies(this IDynamicCommandBuilder builder) 60 | { 61 | builder.Strategies.Clear(); 62 | return builder; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Abstractions/Command/IDynamicCommandBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Chinook.DynamicMvvm 6 | { 7 | /// 8 | /// This is a builder that builds a . 9 | /// 10 | public interface IDynamicCommandBuilder 11 | { 12 | /// 13 | /// Gets name of the command. 14 | /// This cannot be changed. 15 | /// 16 | string Name { get; } 17 | 18 | /// 19 | /// The that will own the resulting . 20 | /// 21 | /// 22 | /// This can be null. 23 | /// 24 | IViewModel ViewModel { get; } 25 | 26 | /// 27 | /// Gets the base stragegy that actually invokes the user execution. 28 | /// This cannot be changed. 29 | /// 30 | IDynamicCommandStrategy BaseStrategy { get; } 31 | 32 | /// 33 | /// The list of strategies that will decorate the . 34 | /// The order is important: the first strategy wraps the second, which wraps the third and so on. 35 | /// 36 | IList Strategies { get; set; } 37 | 38 | /// 39 | /// Creates a new instance of 40 | /// 41 | IDynamicCommand Build(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Abstractions/Command/IDynamicCommandBuilderFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace Chinook.DynamicMvvm 8 | { 9 | /// 10 | /// This interface is used to create . 11 | /// 12 | public interface IDynamicCommandBuilderFactory 13 | { 14 | /// 15 | /// Creates a with the specified 16 | /// that will execute the specified action. 17 | /// 18 | /// The command name. 19 | /// The action to execute. 20 | /// The that will own the newly created . 21 | /// The created . 22 | IDynamicCommandBuilder CreateFromAction( 23 | string name, 24 | Action execute, 25 | IViewModel viewModel = null 26 | ); 27 | 28 | /// 29 | /// Creates a with the specified 30 | /// that will execute the specified action with 31 | /// a parameter of type . 32 | /// 33 | /// The command name. 34 | /// The action to execute. 35 | /// The that will own the newly created . 36 | /// The created . 37 | IDynamicCommandBuilder CreateFromAction( 38 | string name, 39 | Action execute, 40 | IViewModel viewModel = null 41 | ); 42 | 43 | /// 44 | /// Creates a with the specified 45 | /// that will execute the specified task. 46 | /// 47 | /// The command name. 48 | /// The task to execute. 49 | /// The that will own the newly created . 50 | /// The created . 51 | IDynamicCommandBuilder CreateFromTask( 52 | string name, 53 | Func execute, 54 | IViewModel viewModel = null 55 | ); 56 | 57 | /// 58 | /// Creates a with the specified 59 | /// that will execute the specified task with 60 | /// a parameter of type . 61 | /// 62 | /// The command name. 63 | /// The task to execute. 64 | /// The that will own the newly created . 65 | /// The created . 66 | IDynamicCommandBuilder CreateFromTask( 67 | string name, 68 | Func execute, 69 | IViewModel viewModel = null 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Abstractions/Command/IDynamicCommandStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace Chinook.DynamicMvvm 8 | { 9 | /// 10 | /// A is an execution delegate for a . 11 | /// It can be used to delegate the execution of the to different implementations (e.g. action, task, etc.) 12 | /// 13 | public interface IDynamicCommandStrategy : IDisposable 14 | { 15 | /// 16 | /// Determines if the strategy can be executed. 17 | /// 18 | /// The command parameter. 19 | /// The calling command. 20 | /// True if the command can be executed; false otherwise. 21 | bool CanExecute(object parameter, IDynamicCommand command); 22 | 23 | /// 24 | /// Executes the strategy. 25 | /// 26 | /// The . 27 | /// The command parameter. 28 | /// The calling command. 29 | /// 30 | Task Execute(CancellationToken ct, object parameter, IDynamicCommand command); 31 | 32 | /// 33 | /// Occurs when changes occur that affect whether or not the command should execute. 34 | /// 35 | event EventHandler CanExecuteChanged; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Abstractions/Deactivation/IDeactivatable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Chinook.DynamicMvvm.Deactivation 6 | { 7 | /// 8 | /// Represents something that can be deactivated and reactivated. 9 | /// 10 | public interface IDeactivatable 11 | { 12 | /// 13 | /// Gets whether this object is deactivated. 14 | /// This is false by default. 15 | /// 16 | bool IsDeactivated { get; } 17 | 18 | /// 19 | /// Deactivates this object. 20 | /// 21 | void Deactivate(); 22 | 23 | /// 24 | /// Reactivates this object. 25 | /// 26 | void Reactivate(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Abstractions/Deactivation/IDeactivatableViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Chinook.DynamicMvvm.Deactivation 6 | { 7 | /// 8 | /// Represents a ViewModel that implements deactivation. 9 | /// 10 | public interface IDeactivatableViewModel : IViewModel, IDeactivatable 11 | { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Abstractions/DynamicMvvm.Abstractions.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 12 6 | Chinook.DynamicMvvm 7 | nventive 8 | nventive 9 | Chinook.DynamicMvvm.Abstractions 10 | Chinook.DynamicMvvm.Abstractions 11 | Chinook.DynamicMvvm is a collection of extensible MVVM libraries for declarative ViewModels. 12 | true 13 | README.md 14 | mvvm;ios;android;chinook;maui;winui; 15 | Apache-2.0 16 | https://github.com/nventive/Chinook.DynamicMvvm 17 | 18 | 19 | true 20 | true 21 | true 22 | snupkg 23 | 24 | 25 | 26 | 27 | True 28 | \ 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Abstractions/LoggerMessages.Abstractions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Chinook.DynamicMvvm 7 | { 8 | internal static partial class LoggerMessagesAbstractions 9 | { 10 | [LoggerMessage(101, LogLevel.Warning, "Resolving property '{ViewModelTypeName}.{PropertyName}' using reflection on '{ViewModelName}'.")] 11 | public static partial void LogViewModelResolvingPropertyUsingReflection(this ILogger logger, string viewModelTypeName, string propertyName, string viewModelName); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Abstractions/MvvmConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Microsoft.Extensions.Logging; 5 | using Microsoft.Extensions.Logging.Abstractions; 6 | 7 | namespace Chinook.DynamicMvvm 8 | { 9 | /// 10 | /// This class exposes the configuration for the namespace. 11 | /// 12 | public static class DynamicMvvmConfiguration 13 | { 14 | /// 15 | /// Gets or sets the used by all classes under the namespace. 16 | /// The default value is a instance. 17 | /// 18 | public static ILoggerFactory LoggerFactory { get; set; } = new NullLoggerFactory(); 19 | 20 | internal static ILogger Log(this T _) 21 | { 22 | return LoggerFactory.CreateLogger(); 23 | } 24 | 25 | internal static ILogger Log(this Type type) 26 | { 27 | return LoggerFactory.CreateLogger(type); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Abstractions/Property/IDynamicProperty.Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Chinook.DynamicMvvm 6 | { 7 | public static class IDynamicPropertyExtensions 8 | { 9 | /// 10 | /// Subscribes to the changes in the value of the and invokes the 11 | /// callback. 12 | /// 13 | /// The property to subscribe to. 14 | /// The callback. 15 | /// 16 | public static IDisposable Subscribe(this IDynamicProperty property, Action onValueChanged) 17 | { 18 | if (onValueChanged is null) 19 | { 20 | throw new ArgumentNullException(nameof(onValueChanged)); 21 | } 22 | 23 | return new ValueChangedDisposable(property, () => onValueChanged.Invoke(property)); 24 | } 25 | 26 | /// 27 | /// Subscribes to the changes in the value of the and invokes the 28 | /// callback. 29 | /// 30 | /// The type of the property. 31 | /// The property to subscribe to. 32 | /// The callback. 33 | /// 34 | public static IDisposable Subscribe(this IDynamicProperty property, Action> onValueChanged) 35 | { 36 | if (onValueChanged is null) 37 | { 38 | throw new ArgumentNullException(nameof(onValueChanged)); 39 | } 40 | 41 | return new ValueChangedDisposable(property, () => onValueChanged.Invoke(property)); 42 | } 43 | 44 | private class ValueChangedDisposable : IDisposable 45 | { 46 | private IDynamicProperty _property; 47 | private readonly Action _onValueChanged; 48 | 49 | public ValueChangedDisposable(IDynamicProperty property, Action onValueChanged) 50 | { 51 | _property = property ?? throw new ArgumentNullException(nameof(property)); 52 | _onValueChanged = onValueChanged ?? throw new ArgumentNullException(nameof(onValueChanged)); 53 | 54 | _property.ValueChanged += OnValueChanged; 55 | } 56 | 57 | private void OnValueChanged(IDynamicProperty property) 58 | { 59 | _onValueChanged.Invoke(); 60 | } 61 | 62 | public void Dispose() 63 | { 64 | _property.ValueChanged -= OnValueChanged; 65 | _property = null; 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Abstractions/Property/IDynamicProperty.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Chinook.DynamicMvvm 6 | { 7 | /// 8 | /// A represents a property that will notify its subscribers when its value changes. 9 | /// It always has a value that can be accessed synchronously. 10 | /// 11 | public interface IDynamicProperty : IDisposable 12 | { 13 | /// 14 | /// The name of the property. 15 | /// 16 | string Name { get; } 17 | 18 | /// 19 | /// The value of the property. 20 | /// 21 | object Value { get; set; } 22 | 23 | /// 24 | /// Occurs when the value changes. 25 | /// 26 | event DynamicPropertyChangedEventHandler ValueChanged; 27 | } 28 | 29 | /// 30 | /// A typed version of . 31 | /// 32 | /// The type of value. 33 | public interface IDynamicProperty : IDynamicProperty 34 | { 35 | /// 36 | /// The value of the property. 37 | /// 38 | new T Value { get; set; } 39 | } 40 | 41 | /// 42 | /// Occurs when the value of a changes. 43 | /// 44 | /// 45 | public delegate void DynamicPropertyChangedEventHandler(IDynamicProperty property); 46 | } 47 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Abstractions/Property/IDynamicProperty.md: -------------------------------------------------------------------------------- 1 | # IDynamicProperty 2 | 3 | `IDynamicProperty` represents a property that will notify its subscribers when its value changes. 4 | 5 | It **always has a value** that can be accessed synchronously. 6 | 7 | ## Getting started 8 | 9 | To create a `IDynamicProperty`, you use its constructor. 10 | 11 | [Refer to the different implementations below for other ways to create a IDynamicProperty](#implementations). 12 | 13 | ```csharp 14 | // This will create a DynamicProperty with a name of "MyProperty" and a value of 10. 15 | var myProperty = new DynamicProperty("MyProperty", value: 10); 16 | ``` 17 | 18 | To get or set the value of a `IDynamicProperty`, you use its `Value` property. 19 | 20 | ```csharp 21 | // This will return the last value of the DynamicProperty. 22 | var myValue = myProperty.Value; 23 | 24 | // This will set the value of the DynamicProperty to 30. 25 | // The property will notify its subscribers of this change. 26 | myProperty.Value = 30; 27 | ``` 28 | 29 | To be notified when the value of a `IDynamicProperty` changes, you need to subscribe to its `ValueChanged` event. 30 | 31 | ```csharp 32 | // This method will be called everytime the value of the property changes. 33 | void OnValueChanged(IDynamicProperty property) 34 | { 35 | var newValue = property.Value; 36 | 37 | // ... 38 | } 39 | 40 | myProperty.ValueChanged += OnValueChanged; 41 | ``` 42 | 43 | ## Features 44 | 45 | ### Implementations 46 | 47 | There are multiple ways to create a `IDynamicProperty`. 48 | 49 | You can create a property from a **value**. 50 | 51 | ```csharp 52 | // This will create a DynamicProperty with a name of "MyProperty" and a value of 10. 53 | var myProperty = new DynamicProperty("MyProperty", value: 10); 54 | ``` 55 | 56 | You can create a property from an **observable**. 57 | 58 | ```csharp 59 | // This will create a DynamicProperty with a name of "MyPropertyFromObservable" and an initial value of 10. 60 | // When the observable pushes the value of 20, the property will be updated and will notify its subscribers of this change. 61 | var myObservable = Observable.Return(20); 62 | var myPropertyFromObservable = new DynamicPropertyFromObservable("MyPropertyFromObservable", myObservable, initialValue: 10); 63 | ``` 64 | 65 | You can create a property from a **task**. 66 | 67 | ```csharp 68 | // This will create a DynamicProperty with a name of "MyPropertyFromTask" and an initial value of 10. 69 | // When the task completes, the property will be updated with its result and will notify its subscribers of this change. 70 | Task MyTask(CancellationToken ct) => Task.FromResult(20); 71 | var myPropertyFromTask = new DynamicPropertyFromTask("MyPropertyFromTask", MyTask, initialValue: 10); 72 | ``` 73 | 74 | ### Extensions 75 | 76 | You can observe the value of a `IDynamicProperty` using the `Observe` extension. 77 | 78 | ```csharp 79 | myProperty.Observe().Subscribe(value => 80 | { 81 | // This will be called everytime the value of the property changes. 82 | }); 83 | ``` 84 | 85 | You can get and observe the value of a `IDynamicProperty` using the `GetAndObserve` extension. 86 | 87 | ```csharp 88 | myProperty.GetAndObserve().Subscribe(value => 89 | { 90 | // This will be called a first time with the initial value of the property. 91 | // It will then be called everytime the value of the property changes. 92 | }); 93 | ``` 94 | 95 | ### Code Snippets 96 | 97 | You can install the Visual Studio Extension [Chinook Snippets](https://marketplace.visualstudio.com/items?itemName=nventivecorp.ChinookSnippets) and use code snippets to quickly generate dynamic properties. 98 | All snippets related to `IDynamicProperty` start with `ckprop`. 99 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Abstractions/Property/IDynamicPropertyFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace Chinook.DynamicMvvm 8 | { 9 | /// 10 | /// This interface is used to create . 11 | /// 12 | public interface IDynamicPropertyFactory 13 | { 14 | /// 15 | /// Creates a with the specified and . 16 | /// 17 | /// The property type. 18 | /// The property name. 19 | /// The initial value. 20 | /// The that will own the newly created . 21 | /// 22 | IDynamicProperty Create( 23 | string name, 24 | T initialValue = default, 25 | IViewModel viewModel = null 26 | ); 27 | 28 | /// 29 | /// Creates a with the specified and . 30 | /// This property will be updated once the task is complete. 31 | /// 32 | /// The property type. 33 | /// The property name. 34 | /// The property source. 35 | /// The initial value. 36 | /// The that will own the newly created . 37 | /// 38 | IDynamicProperty CreateFromTask( 39 | string name, 40 | Func> source, 41 | T initialValue = default, 42 | IViewModel viewModel = null 43 | ); 44 | 45 | /// 46 | /// Creates a with the specified and . 47 | /// This property will be updated when the observable pushes new values. 48 | /// 49 | /// The property type. 50 | /// The property name. 51 | /// The property source. 52 | /// The initial value. 53 | /// The that will own the newly created . 54 | /// 55 | IDynamicProperty CreateFromObservable( 56 | string name, 57 | IObservable source, 58 | T initialValue = default, 59 | IViewModel viewModel = null 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Abstractions/ViewModel/IDispatcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace Chinook.DynamicMvvm 9 | { 10 | /// 11 | /// An allows to execute code on a specific thread. 12 | /// This is useful when the event needs to be raised on a specific thread. 13 | /// 14 | public interface IDispatcher 15 | { 16 | /// 17 | /// Gets whether or not the thread has dispatcher access. 18 | /// 19 | bool GetHasDispatcherAccess(); 20 | 21 | /// 22 | /// Executes the specified action on a dispatcher thread. 23 | /// 24 | /// The cancellation token. 25 | /// The action to execute. 26 | Task ExecuteOnDispatcher(CancellationToken ct, Action action); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Abstractions/ViewModel/IDispatcherFactory.cs: -------------------------------------------------------------------------------- 1 | namespace Chinook.DynamicMvvm 2 | { 3 | /// 4 | /// This factory is used to abstract the creation of objects. 5 | /// 6 | public interface IDispatcherFactory 7 | { 8 | /// 9 | /// Creates a new using the provided reference. 10 | /// 11 | /// The native view object. 12 | /// A new instance. 13 | IDispatcher Create(object view); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Abstractions/ViewModel/IViewModel.Extensions.Services.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace Chinook.DynamicMvvm 5 | { 6 | /// 7 | /// Extensions on to resolve services. 8 | /// 9 | public static partial class IViewModelExtensions 10 | { 11 | /// 12 | /// Returns the registered service of the specified type . 13 | /// 14 | /// The desired type of service. 15 | /// The providing the . 16 | /// The registered service. 17 | public static T GetService(this IViewModel viewModel) 18 | { 19 | return viewModel.ServiceProvider.GetRequiredService(); 20 | } 21 | 22 | /// 23 | /// Returns the registered service of the specified . 24 | /// 25 | /// The providing the . 26 | /// The desired type of service. 27 | /// The registered service. 28 | public static object GetService(this IViewModel viewModel, Type type) 29 | { 30 | return viewModel.ServiceProvider.GetRequiredService(type); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Abstractions/ViewModel/IViewModel.Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.CompilerServices; 4 | using System.Text; 5 | 6 | namespace Chinook.DynamicMvvm 7 | { 8 | /// 9 | /// Extensions on to get disposables. 10 | /// 11 | public static partial class IViewModelExtensions 12 | { 13 | /// 14 | /// Gets the typed if it exists or default of otherwise. 15 | /// 16 | /// The type of . 17 | /// This . 18 | /// The key associated with the desired disposable. 19 | /// The disposable associated with . 20 | /// The typed disposable when the was found, or default of otherwise. 21 | public static bool TryGetDisposable(this IViewModel viewModel, string key, out TDisposable disposable) 22 | { 23 | if (viewModel.TryGetDisposable(key, out var untypedDisposable)) 24 | { 25 | disposable = (TDisposable)untypedDisposable; 26 | return true; 27 | } 28 | else 29 | { 30 | disposable = default; 31 | return false; 32 | } 33 | } 34 | 35 | /// 36 | /// Gets the typed associated with . 37 | /// 38 | /// The type of . 39 | /// This . 40 | /// The key associated with the desired disposable. 41 | /// The func used to create the . 42 | /// The typed disposable associated with . 43 | /// When any of the parameters is null. 44 | public static TDisposable GetOrCreateDisposable(this IViewModel viewModel, string key, Func create) 45 | where TDisposable : IDisposable 46 | { 47 | if (viewModel is null) 48 | { 49 | throw new ArgumentNullException(nameof(viewModel)); 50 | } 51 | 52 | if (key is null) 53 | { 54 | throw new ArgumentNullException(nameof(key)); 55 | } 56 | 57 | if (create is null) 58 | { 59 | throw new ArgumentNullException(nameof(create)); 60 | } 61 | 62 | if (viewModel.TryGetDisposable(key, out var existingDisposable)) 63 | { 64 | return (TDisposable)existingDisposable; 65 | } 66 | else 67 | { 68 | var disposable = create(); 69 | viewModel.AddDisposable(key, disposable); 70 | return disposable; 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Benchmarks/DynamicMvvm.Benchmarks.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | 12.0 7 | enable 8 | enable 9 | false 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Benchmarks/Program.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Running; 2 | using DynamicMvvm.Benchmarks; 3 | using Chinook.DynamicMvvm; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | using BenchmarkDotNet.Configs; 7 | using BenchmarkDotNet.Order; 8 | 9 | BenchmarkRunner.Run(new[] 10 | { 11 | typeof(ViewModelBaseBenchmark), 12 | typeof(ViewModelExtensionsBenchmark), 13 | }, 14 | ManualConfig 15 | .Create(DefaultConfig.Instance) 16 | .WithOptions(ConfigOptions.JoinSummary) 17 | .WithOrderer(new DefaultOrderer(SummaryOrderPolicy.Declared, MethodOrderPolicy.Declared)) 18 | .HideColumns("Type", "Job", "InvocationCount", "UnrollFactor", "Error", "StdDev", "MaxIterationCount", "MaxWarmupIterationCount") 19 | ); 20 | 21 | // The following section is to profile manually using Visual Studio's debugger. 22 | 23 | //Console.ReadKey(); 24 | 25 | //var serviceProvider = new HostBuilder() 26 | // .ConfigureServices(serviceCollection => serviceCollection 27 | // .AddSingleton() 28 | // .AddSingleton() 29 | // ) 30 | // .Build() 31 | // .Services; 32 | 33 | //var vm = new InitiatedViewModel(); 34 | //vm.Number = 1; 35 | 36 | //var vm1 = new ViewModel("ViewModel", serviceProvider); 37 | //var vm2 = new ViewModel("ViewModel", serviceProvider); 38 | //var value = vm1.NumberResolved; 39 | //value = vm1.Number; 40 | //Console.WriteLine(value); 41 | 42 | //Console.Read(); 43 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Benchmarks/TestViewModelBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.ComponentModel; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using Chinook.DynamicMvvm; 9 | using Microsoft.Extensions.DependencyInjection; 10 | 11 | namespace DynamicMvvm.Benchmarks 12 | { 13 | /// 14 | /// This implementation of IViewModel is used for testing the extension methods of IViewModel. 15 | /// It's not a valid implementation of IViewModel. 16 | /// 17 | public class TestViewModelBase : IViewModel 18 | { 19 | public TestViewModelBase(IServiceProvider? serviceProvider = null) 20 | { 21 | ServiceProvider = serviceProvider; 22 | } 23 | 24 | public string Name => "TestViewModelBase"; 25 | 26 | public virtual IEnumerable> Disposables => Enumerable.Empty>(); 27 | 28 | public IDispatcher? Dispatcher { get; set; } 29 | 30 | public IServiceProvider? ServiceProvider { get; set; } 31 | 32 | public bool IsDisposed { get; set; } 33 | 34 | public bool HasErrors => false; 35 | 36 | public event Action? DispatcherChanged; 37 | public event PropertyChangedEventHandler? PropertyChanged; 38 | public event EventHandler? ErrorsChanged; 39 | 40 | public virtual void AddDisposable(string key, IDisposable disposable) 41 | { 42 | } 43 | 44 | public void ClearErrors(string? propertyName = null) 45 | { 46 | } 47 | 48 | public void Dispose() 49 | { 50 | } 51 | 52 | public IEnumerable GetErrors(string? propertyName) 53 | { 54 | return Enumerable.Empty(); 55 | } 56 | 57 | public void RaisePropertyChanged(string propertyName) 58 | { 59 | } 60 | 61 | public void RemoveDisposable(string key) 62 | { 63 | } 64 | 65 | public void SetErrors(IDictionary> errors) 66 | { 67 | } 68 | 69 | public void SetErrors(string propertyName, IEnumerable errors) 70 | { 71 | } 72 | 73 | public virtual bool TryGetDisposable(string key, out IDisposable? disposable) 74 | { 75 | disposable = default; 76 | return false; 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Benchmarks/ViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reactive.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Chinook.DynamicMvvm; 8 | 9 | namespace DynamicMvvm.Benchmarks; 10 | 11 | public class ViewModel : ViewModelBase 12 | { 13 | public ViewModel(string? name, IServiceProvider serviceProvider) 14 | : base(name, serviceProvider) 15 | { 16 | } 17 | 18 | public ViewModel(IServiceProvider serviceProvider) 19 | : this(name: default, serviceProvider: serviceProvider) 20 | { 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Benchmarks/ViewModelBase.Benchmark.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using Chinook.DynamicMvvm; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Hosting; 5 | 6 | namespace DynamicMvvm.Benchmarks; 7 | 8 | [MemoryDiagnoser] 9 | [MaxIterationCount(36)] 10 | [MaxWarmupCount(16)] 11 | public class ViewModelBaseBenchmark 12 | { 13 | private readonly IServiceProvider _serviceProvider = new HostBuilder() 14 | .ConfigureServices(serviceCollection => serviceCollection 15 | .AddSingleton() 16 | .AddSingleton() 17 | ) 18 | .Build() 19 | .Services; 20 | 21 | private const int ViewModelCount = 2500000; 22 | private ViewModel[]? _viewModelsToDispose; 23 | 24 | [Benchmark] 25 | public IViewModel CreateViewModel() 26 | { 27 | return new ViewModel(_serviceProvider); 28 | } 29 | 30 | [Benchmark] 31 | public IViewModel CreateViewModel_WithExplicitName() 32 | { 33 | return new ViewModel("ViewModel", _serviceProvider); 34 | } 35 | 36 | [IterationSetup(Targets = new[] 37 | { 38 | nameof(DisposeViewModel), 39 | })] 40 | public void SetupViewModel() 41 | { 42 | _viewModelsToDispose = Enumerable 43 | .Range(0, ViewModelCount) 44 | .Select(i => new ViewModel("ViewModel", _serviceProvider)) 45 | .ToArray(); 46 | } 47 | 48 | [Benchmark(OperationsPerInvoke = ViewModelCount)] 49 | [MaxIterationCount(16)] 50 | public void DisposeViewModel() 51 | { 52 | for (var i = 0; i < ViewModelCount; i++) 53 | { 54 | _viewModelsToDispose![i].Dispose(); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/DynamicMvvm.CollectionTracking/DynamicMvvm.CollectionTracking.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 12 6 | Chinook.DynamicMvvm 7 | nventive 8 | nventive 9 | Chinook.DynamicMvvm.CollectionTracking 10 | Chinook.DynamicMvvm.CollectionTracking 11 | Chinook.DynamicMvvm is a collection of extensible MVVM libraries for declarative ViewModels. 12 | true 13 | README.md 14 | mvvm;ios;android;chinook;maui;winui; 15 | Apache-2.0 16 | https://github.com/nventive/Chinook.DynamicMvvm 17 | 18 | 19 | true 20 | true 21 | true 22 | snupkg 23 | 24 | 25 | 26 | 27 | True 28 | \ 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/DynamicMvvm.CollectionTracking/IViewModel.Extensions.CollectionTracking.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Runtime.CompilerServices; 5 | using System.Text; 6 | using Chinook.DynamicMvvm.CollectionTracking; 7 | 8 | namespace Chinook.DynamicMvvm 9 | { 10 | /// 11 | /// This class exposes extensions on related to collection tracking. 12 | /// 13 | public static class CollectionTrackingViewModelExtensions 14 | { 15 | /// 16 | /// Gets a from an observable of list. 17 | /// 18 | /// The collection item type. 19 | /// The owning the underlying adapter. 20 | /// The observable source. 21 | /// The initial value for the collection. 22 | /// The key for the disposable. 23 | /// A updating every time changes. 24 | public static ReadOnlyObservableCollection GetReadOnlyCollectionFromObservable(this IViewModel viewModel, IObservable> source, IEnumerable initialValue, [CallerMemberName] string name = null) 25 | { 26 | if (viewModel.IsDisposed) 27 | { 28 | return null; 29 | } 30 | 31 | var adapter = viewModel.GetOrCreateDisposable(name, () => new ObservableCollectionFromObservableAdapter(viewModel, source, initialValue)); 32 | return adapter.ReadOnlyCollection; 33 | } 34 | 35 | /// 36 | /// Gets a from an observable of list. 37 | /// 38 | /// The collection item type. 39 | /// The owning the underlying adapter. 40 | /// The observable source. 41 | /// The initial value for the collection. 42 | /// The key for the disposable. 43 | /// A updating every time changes. 44 | public static ObservableCollection GetObservableCollectionFromObservable(this IViewModel viewModel, IObservable> source, IEnumerable initialValue, [CallerMemberName] string name = null) 45 | { 46 | if (viewModel.IsDisposed) 47 | { 48 | return null; 49 | } 50 | 51 | var adapter = viewModel.GetOrCreateDisposable(name, () => new ObservableCollectionFromObservableAdapter(viewModel, source, initialValue)); 52 | return adapter.Collection; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/DynamicMvvm.FluentValidation/DynamicMvvm.FluentValidation.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 12 6 | Chinook.DynamicMvvm 7 | nventive 8 | nventive 9 | Chinook.DynamicMvvm.FluentValidation 10 | Chinook.DynamicMvvm.FluentValidation 11 | Chinook.DynamicMvvm is a collection of extensible MVVM libraries for declarative ViewModels. 12 | true 13 | README.md 14 | mvvm;ios;android;chinook;maui;winui; 15 | Apache-2.0 16 | https://github.com/nventive/Chinook.DynamicMvvm 17 | 18 | 19 | true 20 | true 21 | true 22 | snupkg 23 | 24 | 25 | 26 | 27 | True 28 | \ 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/DynamicMvvm.FluentValidation/LoggerMessages.FluentValidation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Chinook.DynamicMvvm 7 | { 8 | internal static partial class LoggerMessages 9 | { 10 | [LoggerMessage(201, LogLevel.Error, "Validation failed for property '{PropertyName}'.")] 11 | public static partial void LogValidationFailed(this ILogger logger, string propertyName, Exception exception); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Reactive/Command/IDynamicCommandExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using System.Collections.Generic; 4 | using System.Reactive.Linq; 5 | 6 | namespace Chinook.DynamicMvvm 7 | { 8 | /// 9 | /// Extensions on 10 | /// 11 | public static class IDynamicCommandExtensions 12 | { 13 | /// 14 | /// Observes if the is currently executing. 15 | /// 16 | /// 17 | /// Observable of boolean 18 | public static IObservable ObserveIsExecuting(this IDynamicCommand command) 19 | { 20 | return Observable.FromEventPattern( 21 | h => command.IsExecutingChanged += h, 22 | h => command.IsExecutingChanged -= h 23 | ) 24 | .Select(p => command.IsExecuting); 25 | } 26 | 27 | /// 28 | /// Gets and observes if the is currently executing. 29 | /// 30 | /// 31 | /// Observable of boolean 32 | public static IObservable GetAndObserveIsExecuting(this IDynamicCommand command) 33 | { 34 | return ObserveIsExecuting(command).StartWith(command.IsExecuting); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Reactive/Deactivation/DeactivatableObservable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reactive.Disposables; 4 | using System.Text; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace Chinook.DynamicMvvm.Deactivation 8 | { 9 | /// 10 | /// This is an than can be deactivated. 11 | /// When deactivated, this observable unsubscribes from its inner source. 12 | /// When reactivated, this observable re-subscribes to its inner source. 13 | /// 14 | /// The type of data. 15 | public class DeactivatableObservable : IObservable, IDeactivatable, IDisposable 16 | { 17 | private readonly IObservable _source; 18 | 19 | private IDisposable _subscription; 20 | private IObserver _observer; 21 | private bool _isDisposingOrDisposed; 22 | 23 | /// 24 | /// Creates a new instance of . 25 | /// 26 | /// The source that will be subscribed to when is false. 27 | public DeactivatableObservable(IObservable source) 28 | { 29 | _source = source ?? throw new ArgumentNullException(paramName: nameof(source)); 30 | } 31 | 32 | /// 33 | public bool IsDeactivated { get; private set; } = false; 34 | 35 | /// 36 | public IDisposable Subscribe(IObserver observer) 37 | { 38 | if (_observer != null) 39 | { 40 | // This is sufficient for our use cases. Supporting more would complexify the code a lot. 41 | throw new NotSupportedException("DeactivatableObservable only supports 1 subscription."); 42 | } 43 | 44 | _observer = observer; 45 | _subscription = _source.Subscribe(observer); 46 | return new CompositeDisposable( 47 | _subscription, 48 | Disposable.Create(() => _observer = null) 49 | ); 50 | } 51 | 52 | /// 53 | public void Deactivate() 54 | { 55 | if (IsDeactivated) 56 | { 57 | return; 58 | } 59 | 60 | _subscription?.Dispose(); 61 | 62 | IsDeactivated = true; 63 | 64 | typeof(IDeactivatable).Log().LogDeactivatedObservable(typeof(T).Name); 65 | } 66 | 67 | /// 68 | public void Reactivate() 69 | { 70 | if (!IsDeactivated) 71 | { 72 | return; 73 | } 74 | 75 | IsDeactivated = false; 76 | 77 | if (_observer != null) 78 | { 79 | _subscription = _source.Subscribe(_observer); 80 | } 81 | 82 | typeof(IDeactivatable).Log().LogReactivatedObservable(typeof(T).Name); 83 | } 84 | 85 | /// 86 | public void Dispose() 87 | { 88 | if (_isDisposingOrDisposed) 89 | { 90 | return; 91 | } 92 | 93 | _isDisposingOrDisposed = true; 94 | _subscription?.Dispose(); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Reactive/Deactivation/IObservable.Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Chinook.DynamicMvvm.Deactivation; 5 | 6 | namespace System.Reactive.Linq 7 | { 8 | /// 9 | /// This class exposes extensions methods on in the context of . 10 | /// 11 | public static class ChinookDynamicMvvmDeactivationObservableExtensions 12 | { 13 | /// 14 | /// Automatically disconnects when deactivates.
15 | /// Automatically reconnects when reactivates. 16 | ///
17 | /// The type of data. 18 | /// The observable source. 19 | /// The view model controlling the deactivation and reactivation of the observable. 20 | /// 21 | /// An observable sequence that automatically disconnects and reconnects based on the state of . 22 | /// 23 | public static IObservable DeactivateWith(this IObservable source, IDeactivatableViewModel viewModel) 24 | { 25 | var deactivatableObservable = new DeactivatableObservable(source); 26 | viewModel.AddDisposable(Guid.NewGuid().ToString(), deactivatableObservable); 27 | return deactivatableObservable; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Reactive/DynamicMvvm.Reactive.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 12 6 | Chinook.DynamicMvvm 7 | nventive 8 | nventive 9 | Chinook.DynamicMvvm.Reactive 10 | Chinook.DynamicMvvm.Reactive 11 | Chinook.DynamicMvvm is a collection of extensible MVVM libraries for declarative ViewModels. 12 | true 13 | README.md 14 | mvvm;ios;android;chinook;maui;winui; 15 | Apache-2.0 16 | https://github.com/nventive/Chinook.DynamicMvvm 17 | 18 | 19 | true 20 | true 21 | true 22 | snupkg 23 | 24 | 25 | 26 | 27 | True 28 | \ 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Reactive/LoggerMessages.Reactive.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Chinook.DynamicMvvm 7 | { 8 | internal static partial class LoggerMessagesReactive 9 | { 10 | [LoggerMessage(301, LogLevel.Debug, "Deactivated observable of type '{TypeName}'.")] 11 | public static partial void LogDeactivatedObservable(this ILogger logger, string typeName); 12 | 13 | [LoggerMessage(302, LogLevel.Debug, "Reactivated observable of type '{TypeName}'.")] 14 | public static partial void LogReactivatedObservable(this ILogger logger, string typeName); 15 | 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Shared/DynamicMvvm.Shared.projitems: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 5 | true 6 | 8bcf881b-035b-4572-a0b5-27c379debee8 7 | 8 | 9 | Shared 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Shared/DynamicMvvm.Shared.shproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8bcf881b-035b-4572-a0b5-27c379debee8 5 | 14.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Shared/PreserveAttribute.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable IDE1006 // Naming Styles 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace Chinook.DynamicMvvm 7 | { 8 | /// 9 | /// This attribute is used to avoid the mono linker to remove members. 10 | /// 11 | [AttributeUsage(AttributeTargets.All)] 12 | internal sealed class PreserveAttribute : Attribute 13 | { 14 | public bool AllMembers; 15 | } 16 | } 17 | #pragma warning restore IDE1006 // Naming Styles 18 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Tests/CollectionTracking/ObservableCollectionFromObservableAdapterTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Collections.Specialized; 5 | using System.Linq; 6 | using System.Reactive.Subjects; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using Chinook.DynamicMvvm.CollectionTracking; 10 | using FluentAssertions; 11 | using Xunit; 12 | 13 | namespace Chinook.DynamicMvvm.Tests.CollectionTracking 14 | { 15 | public class ObservableCollectionFromObservableAdapterTests 16 | { 17 | public static IEnumerable InitialValues => 18 | new[] 19 | { 20 | new []{ Enumerable.Empty() }, 21 | new []{ new string[] { "Hello", "Bonjour" } }, 22 | new []{ Enumerable.Range(0,10).Select(i => i.ToString()) }, 23 | }; 24 | 25 | [Theory] 26 | [MemberData(nameof(InitialValues))] 27 | public void Initializes_with_initial_value(IEnumerable initialValue) 28 | { 29 | var vm = new ViewModelBase(); 30 | var subject = new Subject>(); 31 | 32 | var sut = new ObservableCollectionFromObservableAdapter(vm, subject, initialValue); 33 | 34 | sut.Collection.Should().BeEquivalentTo(initialValue); 35 | sut.ReadOnlyCollection.Should().BeEquivalentTo(initialValue); 36 | } 37 | 38 | [Fact] 39 | public void Updates_when_observable_pushes() 40 | { 41 | // Arrange 42 | var vm = new ViewModelBase(); 43 | var subject = new Subject>(); 44 | var sut = new ObservableCollectionFromObservableAdapter(vm, subject, initialValue: Enumerable.Empty()); 45 | 46 | // Act 47 | subject.OnNext(new[] { "Hello" }); 48 | 49 | // Assert 50 | var expected = new [] { "Hello" }; 51 | sut.Collection.Should().BeEquivalentTo(expected); 52 | sut.ReadOnlyCollection.Should().BeEquivalentTo(expected); 53 | } 54 | 55 | [Fact] 56 | public void Pushing_a_new_list_with_an_additional_item_is_reported_as_a_single_add() 57 | { 58 | // Arrange 59 | var initialValue = Enumerable.Range(0,10).Select(i => i.ToString()).ToImmutableList(); 60 | var vm = new ViewModelBase(); 61 | var subject = new Subject>(); 62 | var sut = new ObservableCollectionFromObservableAdapter(vm, subject, initialValue); 63 | var incc = sut.ReadOnlyCollection as INotifyCollectionChanged; 64 | var args = default(NotifyCollectionChangedEventArgs); 65 | incc.CollectionChanged += OnCollectionChanged; 66 | 67 | // Act 68 | subject.OnNext(initialValue.Insert(0, "-1")); 69 | 70 | // Assert 71 | args.Action.Should().Be(NotifyCollectionChangedAction.Add); 72 | args.NewStartingIndex.Should().Be(0); 73 | args.NewItems.Should().ContainEquivalentOf("-1"); 74 | 75 | void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) 76 | { 77 | args = e; 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Tests/Command/DynamicCommandBuilderExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using FluentAssertions; 7 | using Xunit; 8 | 9 | namespace Chinook.DynamicMvvm.Tests.Command 10 | { 11 | public class DynamicCommandBuilderExtensionsTests 12 | { 13 | [Fact] 14 | public void WithStrategy_properly_adds() 15 | { 16 | var builder = new DynamicCommandBuilder("myCommand", new ActionCommandStrategy(() => { }), null); 17 | 18 | builder.Strategies.Should().BeEmpty(); 19 | 20 | builder.WithStrategy(new BackgroundCommandStrategy()); 21 | 22 | builder.Strategies.Should().ContainSingle(); 23 | } 24 | 25 | [Fact] 26 | public void WithoutStrategy_properly_removes() 27 | { 28 | var builder = new DynamicCommandBuilder("myCommand", new ActionCommandStrategy(() => { }), null) 29 | .OnBackgroundThread() 30 | .Locked() 31 | .DisableWhileExecuting(); 32 | 33 | builder.Strategies.Should().HaveCount(3); 34 | 35 | builder.WithoutStrategy(); 36 | 37 | builder.Strategies.Should().HaveCount(2); 38 | } 39 | 40 | [Fact] 41 | public void ClearStrategies_properly_clears() 42 | { 43 | var builder = new DynamicCommandBuilder("myCommand", new ActionCommandStrategy(() => { }), null) 44 | .OnBackgroundThread() 45 | .Locked() 46 | .DisableWhileExecuting(); 47 | 48 | 49 | builder.Strategies.Should().NotBeEmpty(); 50 | 51 | builder.ClearStrategies(); 52 | 53 | builder.Strategies.Should().BeEmpty(); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Tests/Command/DynamicCommandReactiveTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using FluentAssertions; 7 | using Chinook.DynamicMvvm.Tests.Helpers; 8 | using Xunit; 9 | 10 | namespace Chinook.DynamicMvvm.Tests.Command 11 | { 12 | public class DynamicCommandReactiveTests 13 | { 14 | private const string DefaultCommandName = nameof(DefaultCommandName); 15 | 16 | [Fact] 17 | public async Task It_Observes_IsExecuting() 18 | { 19 | var receivedValues = new List(); 20 | var strategy = new TestCommandStrategy(); 21 | 22 | var command = new DynamicCommand(DefaultCommandName, strategy); 23 | 24 | var testSubscriber = new TestSubscriber(onNext: t => receivedValues.Add(t)); 25 | 26 | var subscription = command 27 | .ObserveIsExecuting() 28 | .Subscribe(testSubscriber); 29 | 30 | using (subscription) 31 | { 32 | await command.Execute(); 33 | 34 | receivedValues.Count().Should().Be(2); 35 | receivedValues[0].Should().BeTrue(); 36 | receivedValues[1].Should().BeFalse(); 37 | } 38 | } 39 | 40 | [Fact] 41 | public async Task It_Gets_And_Observes_IsExecuting() 42 | { 43 | var receivedValues = new List(); 44 | var strategy = new TestCommandStrategy(); 45 | 46 | var command = new DynamicCommand(DefaultCommandName, strategy); 47 | 48 | var testSubscriber = new TestSubscriber(onNext: t => receivedValues.Add(t)); 49 | 50 | var subscription = command 51 | .GetAndObserveIsExecuting() 52 | .Subscribe(testSubscriber); 53 | 54 | using (subscription) 55 | { 56 | await command.Execute(); 57 | 58 | receivedValues.Count().Should().Be(3); 59 | receivedValues[0].Should().BeFalse(); 60 | receivedValues[1].Should().BeTrue(); 61 | receivedValues[2].Should().BeFalse(); 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Tests/Command/Strategies/ActionCommandStrategyTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using FluentAssertions; 8 | using Xunit; 9 | 10 | namespace Chinook.DynamicMvvm.Tests.Command.Strategies 11 | { 12 | public class ActionCommandStrategyTests 13 | { 14 | private const string DefaultCommandName = nameof(DefaultCommandName); 15 | 16 | [Fact] 17 | public async Task It_Executes_Without_Parameter() 18 | { 19 | var isExecuted = false; 20 | 21 | var strategy = new ActionCommandStrategy(() => isExecuted = true); 22 | var command = new DynamicCommand(DefaultCommandName, strategy); 23 | 24 | await command.Execute(); 25 | 26 | isExecuted.Should().BeTrue(); 27 | } 28 | 29 | [Fact] 30 | public async Task It_Executes_With_Parameter() 31 | { 32 | var executeStrategyParameter = default(object); 33 | 34 | var strategy = new ActionCommandStrategy( 35 | execute: p => executeStrategyParameter = p 36 | ); 37 | 38 | var command = new DynamicCommand(DefaultCommandName, strategy); 39 | 40 | var parameter = new object(); 41 | await command.Execute(parameter); 42 | 43 | executeStrategyParameter.Should().Be(parameter); 44 | } 45 | 46 | [Fact] 47 | public async Task It_Executes_With_Parameter_T() 48 | { 49 | var executeStrategyParameter = default(TestParameter); 50 | 51 | var strategy = new ActionCommandStrategy( 52 | execute: p => executeStrategyParameter = p 53 | ); 54 | 55 | var command = new DynamicCommand(DefaultCommandName, strategy); 56 | 57 | var parameter = new TestParameter(); 58 | await command.Execute(parameter); 59 | 60 | executeStrategyParameter.Should().Be(parameter); 61 | } 62 | 63 | private class TestParameter { } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Tests/Command/Strategies/BackgroundCommandStrategyTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using FluentAssertions; 8 | using Chinook.DynamicMvvm.Tests.Helpers; 9 | using Xunit; 10 | 11 | namespace Chinook.DynamicMvvm.Tests.Command.Strategies 12 | { 13 | public class BackgroundCommandStrategyTests 14 | { 15 | private const string DefaultCommandName = nameof(DefaultCommandName); 16 | 17 | [Fact] 18 | public async Task It_Executes_On_Background_Thread() 19 | { 20 | var isRunningOnThreadPool = false; 21 | 22 | var testStrategy = new TestCommandStrategy( 23 | onExecute: (_, __, ___) => 24 | { 25 | isRunningOnThreadPool = Thread.CurrentThread.IsThreadPoolThread; 26 | 27 | return Task.CompletedTask; 28 | } 29 | ); 30 | 31 | var strategy = new BackgroundCommandStrategy() 32 | { 33 | InnerStrategy = testStrategy 34 | }; 35 | 36 | var command = new DynamicCommand(DefaultCommandName, strategy); 37 | 38 | await command.Execute(); 39 | 40 | isRunningOnThreadPool.Should().BeTrue(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Tests/Command/Strategies/CanExecuteCommandStrategyTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using FluentAssertions; 7 | using Chinook.DynamicMvvm.Tests.Helpers; 8 | using Xunit; 9 | 10 | namespace Chinook.DynamicMvvm.Tests.Command.Strategies 11 | { 12 | public class CanExecuteCommandStrategyTests 13 | { 14 | private const string DefaultCommandName = nameof(DefaultCommandName); 15 | private const string DefaultPropertyName = nameof(DefaultPropertyName); 16 | 17 | [Fact] 18 | public void It_Raises_CanExecute_When_Property_Changes() 19 | { 20 | var canExecute = true; 21 | var property = new DynamicProperty(DefaultPropertyName, false); 22 | var testStrategy = new TestCommandStrategy(); 23 | 24 | var strategy = new CanExecuteCommandStrategy(property) 25 | { 26 | InnerStrategy = testStrategy 27 | }; 28 | 29 | var command = new DynamicCommand(DefaultCommandName, strategy); 30 | 31 | command.CanExecuteChanged += OnCanExecuteChanged; 32 | 33 | canExecute = command.CanExecute(null); 34 | canExecute.Should().BeFalse(); 35 | 36 | property.Value = true; 37 | canExecute.Should().BeTrue(); 38 | 39 | property.Value = false; 40 | canExecute.Should().BeFalse(); 41 | 42 | void OnCanExecuteChanged(object sender, EventArgs e) 43 | { 44 | canExecute = command.CanExecute(null); 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Tests/Command/Strategies/CancelPreviousCommandStrategyTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using FluentAssertions; 8 | using Chinook.DynamicMvvm.Tests.Helpers; 9 | using Xunit; 10 | 11 | namespace Chinook.DynamicMvvm.Tests.Command.Strategies 12 | { 13 | public class CancelPreviousCommandStrategyTests 14 | { 15 | private const string DefaultCommandName = nameof(DefaultCommandName); 16 | 17 | [Fact] 18 | public async Task It_Cancels_Previous() 19 | { 20 | var tasks = new List>(); 21 | 22 | var testStrategy = new TestCommandStrategy(onExecute: async (ct, _, __) => 23 | { 24 | var newTask = new TaskCompletionSource(); 25 | 26 | tasks.Add(newTask); 27 | 28 | using (ct.Register(() => newTask.TrySetCanceled())) 29 | { 30 | await newTask.Task; 31 | } 32 | }); 33 | 34 | var strategy = new CancelPreviousCommandStrategy() 35 | { 36 | InnerStrategy = testStrategy 37 | }; 38 | 39 | var command = new DynamicCommand(DefaultCommandName, strategy); 40 | 41 | // Start a first execution 42 | var firstExecution = command.Execute(); 43 | var firstTask = tasks.ElementAt(0); 44 | 45 | firstTask.Task.IsCanceled.Should().BeFalse(); 46 | 47 | // Start a second execution 48 | var secondExecution = command.Execute(); 49 | var secondTask = tasks.ElementAt(1); 50 | 51 | firstTask.Task.IsCanceled.Should().BeTrue(); 52 | secondTask.Task.IsCanceled.Should().BeFalse(); 53 | 54 | // Start a third execution 55 | var thirdExecution = command.Execute(); 56 | var thirdTask = tasks.ElementAt(2); 57 | 58 | secondTask.Task.IsCanceled.Should().BeTrue(); 59 | thirdTask.Task.IsCanceled.Should().BeFalse(); 60 | 61 | // Complete the third execution 62 | thirdTask.TrySetResult(null); 63 | 64 | await Task.WhenAll(firstExecution, secondExecution, thirdExecution); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Tests/Command/Strategies/DisableWhileExecutingCommandStrategyTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using FluentAssertions; 8 | using Chinook.DynamicMvvm.Tests.Helpers; 9 | using Xunit; 10 | 11 | namespace Chinook.DynamicMvvm.Tests.Command.Strategies 12 | { 13 | public class DisableWhileExecutingCommandStrategyTests 14 | { 15 | private const string DefaultCommandName = nameof(DefaultCommandName); 16 | 17 | [Fact] 18 | public async Task It_Disables_While_Executing() 19 | { 20 | var taskCompletionSource = new TaskCompletionSource(); 21 | 22 | var testStrategy = new TestCommandStrategy( 23 | onExecute: (_, __, ___) => taskCompletionSource.Task 24 | ); 25 | 26 | var strategy = new DisableWhileExecutingCommandStrategy() 27 | { 28 | InnerStrategy = testStrategy 29 | }; 30 | 31 | var command = new DynamicCommand(DefaultCommandName, strategy); 32 | 33 | command.CanExecuteChanged += OnCanExecuteChanged; 34 | 35 | var canExecute = command.CanExecute(null); 36 | 37 | // The command should be enabled 38 | canExecute.Should().BeTrue(); 39 | 40 | // We execute the command 41 | var commandExecution = command.Execute(); 42 | 43 | // The command should be disabled 44 | canExecute.Should().BeFalse(); 45 | 46 | // The command completes 47 | taskCompletionSource.TrySetResult(null); 48 | 49 | await commandExecution; 50 | 51 | // The command should be enabled 52 | canExecute.Should().BeTrue(); 53 | 54 | void OnCanExecuteChanged(object sender, EventArgs e) 55 | { 56 | canExecute = command.CanExecute(null); 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Tests/Command/Strategies/ErrorHandlerCommandStrategyTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using FluentAssertions; 8 | using Chinook.DynamicMvvm.Tests.Helpers; 9 | using Xunit; 10 | 11 | namespace Chinook.DynamicMvvm.Tests.Command.Strategies 12 | { 13 | public class ErrorHandlerCommandStrategyTests 14 | { 15 | private const string DefaultCommandName = nameof(DefaultCommandName); 16 | 17 | [Fact] 18 | public async Task It_Catches_Errors_If_Any() 19 | { 20 | var exception = new MyCustomException(); 21 | var receivedCommand = default(IDynamicCommand); 22 | var receivedException = default(Exception); 23 | 24 | var testStrategy = new TestCommandStrategy( 25 | onExecute: (_, __, ___) => throw exception 26 | ); 27 | 28 | var strategy = new ErrorHandlerCommandStrategy(new DynamicCommandErrorHandler((ct, c, e) => 29 | { 30 | receivedCommand = c; 31 | receivedException = e; 32 | 33 | return Task.CompletedTask; 34 | })) 35 | { 36 | InnerStrategy = testStrategy 37 | }; 38 | 39 | var command = new DynamicCommand(DefaultCommandName, strategy); 40 | 41 | await command.Execute(); 42 | 43 | receivedCommand.Should().Be(command); 44 | receivedException.Should().Be(exception); 45 | } 46 | 47 | [Fact] 48 | public async Task It_Doesnt_Catch_Errors_If_None() 49 | { 50 | var catchedErrors = false; 51 | 52 | var testStrategy = new TestCommandStrategy(); 53 | 54 | var strategy = new ErrorHandlerCommandStrategy(new DynamicCommandErrorHandler((ct, c, e) => 55 | { 56 | catchedErrors = true; 57 | 58 | return Task.CompletedTask; 59 | })) 60 | { 61 | InnerStrategy = testStrategy 62 | }; 63 | 64 | var command = new DynamicCommand(DefaultCommandName, strategy); 65 | 66 | await command.Execute(); 67 | 68 | catchedErrors.Should().BeFalse(); 69 | } 70 | 71 | private class MyCustomException : Exception { } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Tests/Command/Strategies/LockCommandStrategyTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using System.Windows.Input; 8 | using FluentAssertions; 9 | using Chinook.DynamicMvvm.Tests.Helpers; 10 | using Xunit; 11 | 12 | namespace Chinook.DynamicMvvm.Tests.Command.Strategies 13 | { 14 | public class LockCommandStrategyTests 15 | { 16 | private const string DefaultCommandName = nameof(DefaultCommandName); 17 | 18 | [Fact] 19 | public async Task It_Has_Single_Execution() 20 | { 21 | var concurrentExecutions = 0; 22 | var hadConcurrentExecutions = false; 23 | 24 | var tasks = new[] 25 | { 26 | new TaskCompletionSource(), 27 | new TaskCompletionSource(), 28 | new TaskCompletionSource(), 29 | new TaskCompletionSource(), 30 | }; 31 | 32 | var testStrategy = new TestCommandStrategy(onExecute: async (_, i, ___) => 33 | { 34 | if (Interlocked.Increment(ref concurrentExecutions) > 1) 35 | { 36 | hadConcurrentExecutions = true; 37 | } 38 | 39 | await tasks[(int)i].Task; 40 | 41 | Interlocked.Decrement(ref concurrentExecutions); 42 | }); 43 | 44 | var strategy = new LockCommandStrategy() 45 | { 46 | InnerStrategy = testStrategy 47 | }; 48 | 49 | var command = new DynamicCommand(DefaultCommandName, strategy); 50 | 51 | var executions = tasks 52 | .Select((t, i) => command.Execute(i)) 53 | .ToArray(); 54 | 55 | Array.ForEach(tasks, t => t.TrySetResult(null)); 56 | 57 | await Task.WhenAll(executions); 58 | 59 | hadConcurrentExecutions.Should().BeFalse(); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Tests/Command/Strategies/RaiseCanExecuteOnDispatcherCommandStrategyTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Chinook.DynamicMvvm.Tests.Helpers; 7 | using FluentAssertions; 8 | using Xunit; 9 | 10 | namespace Chinook.DynamicMvvm.Tests.Command.Strategies 11 | { 12 | public class RaiseCanExecuteOnDispatcherCommandStrategyTests 13 | { 14 | [Fact] 15 | public void Requires_a_non_null_ViewModel() 16 | { 17 | Assert.Throws(() => new RaiseCanExecuteOnDispatcherCommandStrategy(viewModel: null)); 18 | } 19 | 20 | [Fact] 21 | public void Dispatches_to_view_when_not_already_on_dispatcher() 22 | { 23 | var vm = new ViewModelBase(); 24 | var sut = new RaiseCanExecuteOnDispatcherCommandStrategy(vm); 25 | var inner = new TestCommandStrategy(); 26 | var dispatched = false; 27 | sut.InnerStrategy = inner; 28 | 29 | vm.Dispatcher = new TestDispatcher(hasDispatcherAccess: false, OnExecuteOnDispatcher); 30 | 31 | inner.RaiseCanExecuteChanged(); 32 | 33 | dispatched.Should().BeTrue(); 34 | 35 | void OnExecuteOnDispatcher(Action action) 36 | { 37 | dispatched = true; 38 | action(); 39 | } 40 | } 41 | 42 | [Fact] 43 | public void Does_not_dispatch_to_view_when_already_on_dispatcher() 44 | { 45 | var vm = new ViewModelBase(); 46 | var sut = new RaiseCanExecuteOnDispatcherCommandStrategy(vm); 47 | var inner = new TestCommandStrategy(); 48 | var dispatched = false; 49 | sut.InnerStrategy = inner; 50 | 51 | vm.Dispatcher = new TestDispatcher(hasDispatcherAccess: true, OnExecuteOnDispatcher); 52 | 53 | inner.RaiseCanExecuteChanged(); 54 | 55 | dispatched.Should().BeFalse(); 56 | 57 | void OnExecuteOnDispatcher(Action action) 58 | { 59 | dispatched = true; 60 | action(); 61 | } 62 | } 63 | 64 | [Fact] 65 | public void Raises_even_if_view_is_null() 66 | { 67 | var vm = new ViewModelBase(); 68 | var sut = new RaiseCanExecuteOnDispatcherCommandStrategy(vm); 69 | var inner = new TestCommandStrategy(); 70 | var raised = false; 71 | sut.InnerStrategy = inner; 72 | sut.CanExecuteChanged += OnCanExecuteChanged; 73 | 74 | inner.RaiseCanExecuteChanged(); 75 | 76 | raised.Should().BeTrue(); 77 | 78 | void OnCanExecuteChanged(object sender, EventArgs e) 79 | { 80 | raised = true; 81 | } 82 | } 83 | 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Tests/Command/Strategies/SkipWhileExecutingCommandStrategyTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Windows.Input; 7 | using FluentAssertions; 8 | using Chinook.DynamicMvvm.Tests.Helpers; 9 | using Xunit; 10 | 11 | namespace Chinook.DynamicMvvm.Tests.Command.Strategies 12 | { 13 | public class SkipWhileExecutingCommandStrategyTests 14 | { 15 | private const string DefaultCommandName = nameof(DefaultCommandName); 16 | 17 | [Fact] 18 | public async Task It_Skips_While_Executing() 19 | { 20 | var actualExecutions = 0; 21 | 22 | var tasks = new[] 23 | { 24 | new TaskCompletionSource(), 25 | new TaskCompletionSource(), 26 | new TaskCompletionSource(), 27 | new TaskCompletionSource(), 28 | }; 29 | 30 | var testStrategy = new TestCommandStrategy( 31 | onExecute: async (_, i, ___) => 32 | { 33 | actualExecutions++; 34 | 35 | await tasks[(int)i].Task; 36 | } 37 | ); 38 | 39 | 40 | var strategy = new SkipWhileExecutingCommandStrategy() 41 | { 42 | InnerStrategy = testStrategy 43 | }; 44 | 45 | var command = new DynamicCommand(DefaultCommandName, strategy); 46 | 47 | var executions = tasks 48 | .Select((t, i) => command.Execute(i)) 49 | .ToArray(); 50 | 51 | Array.ForEach(tasks, t => t.TrySetResult(null)); 52 | 53 | await Task.WhenAll(executions); 54 | 55 | actualExecutions.Should().Be(1); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Tests/Command/Strategies/TaskCommandStrategyTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using System.Windows.Input; 8 | using FluentAssertions; 9 | using Xunit; 10 | 11 | namespace Chinook.DynamicMvvm.Tests.Command.Strategies 12 | { 13 | public class TaskCommandStrategyTests 14 | { 15 | private const string DefaultCommandName = nameof(DefaultCommandName); 16 | 17 | [Fact] 18 | public async Task It_Executes_Without_Parameter() 19 | { 20 | var isExecuted = false; 21 | 22 | var strategy = new TaskCommandStrategy(ct => 23 | { 24 | isExecuted = true; 25 | 26 | return Task.CompletedTask; 27 | }); 28 | 29 | var command = new DynamicCommand(DefaultCommandName, strategy); 30 | 31 | await command.Execute(); 32 | 33 | isExecuted.Should().BeTrue(); 34 | } 35 | 36 | [Fact] 37 | public async Task It_Executes_With_Parameter() 38 | { 39 | var executeStrategyParameter = default(object); 40 | 41 | var strategy = new TaskCommandStrategy( 42 | execute: (ct, p) => 43 | { 44 | executeStrategyParameter = p; 45 | 46 | return Task.CompletedTask; 47 | } 48 | ); 49 | 50 | var command = new DynamicCommand(DefaultCommandName, strategy); 51 | 52 | var parameter = new object(); 53 | await command.Execute(parameter); 54 | 55 | executeStrategyParameter.Should().Be(parameter); 56 | } 57 | 58 | [Fact] 59 | public async Task It_Executes_With_Parameter_T() 60 | { 61 | var executeStrategyParameter = default(TestParameter); 62 | 63 | var strategy = new TaskCommandStrategy( 64 | execute: (ct, p) => 65 | { 66 | executeStrategyParameter = p; 67 | 68 | return Task.CompletedTask; 69 | } 70 | ); 71 | 72 | var command = new DynamicCommand(DefaultCommandName, strategy); 73 | 74 | var parameter = new TestParameter(); 75 | await command.Execute(parameter); 76 | 77 | executeStrategyParameter.Should().Be(parameter); 78 | } 79 | 80 | private class TestParameter { } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Tests/DynamicMvvm.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | 12.0 6 | false 7 | Chinook.DynamicMvvm.Tests 8 | Chinook.DynamicMvvm.Tests 9 | True 10 | 11 | 12 | 13 | 14 | all 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | all 28 | runtime; build; native; contentfiles; analyzers; buildtransitive 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Tests/Helpers/TestCommandStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace Chinook.DynamicMvvm.Tests.Helpers 9 | { 10 | public class TestCommandStrategy : IDynamicCommandStrategy 11 | { 12 | private readonly Func _onCanExecute; 13 | private readonly Func _onExecute; 14 | private readonly Action _onDispose; 15 | 16 | public TestCommandStrategy( 17 | Func onCanExecute = null, 18 | Func onExecute = null, 19 | Action onDispose = null 20 | ) 21 | { 22 | _onCanExecute = onCanExecute; 23 | _onExecute = onExecute; 24 | _onDispose = onDispose; 25 | } 26 | 27 | public bool IsDisposed { get; private set; } 28 | 29 | public bool IsExecuted { get; private set; } 30 | 31 | public event EventHandler CanExecuteChanged; 32 | 33 | public bool CanExecute(object parameter, IDynamicCommand command) => _onCanExecute != null 34 | ? _onCanExecute.Invoke(parameter, command) 35 | : true; 36 | 37 | public Task Execute(CancellationToken ct, object parameter, IDynamicCommand command) => _onExecute != null 38 | ? _onExecute.Invoke(ct, parameter, command) 39 | : Task.CompletedTask; 40 | 41 | public void RaiseCanExecuteChanged() 42 | { 43 | CanExecuteChanged?.Invoke(this, EventArgs.Empty); 44 | } 45 | 46 | public void Dispose() => _onDispose?.Invoke(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Tests/Helpers/TestDisposable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Chinook.DynamicMvvm.Tests.Helpers 8 | { 9 | public class TestDisposable : IDisposable 10 | { 11 | private readonly Action _onDispose; 12 | 13 | public TestDisposable(Action onDispose = null) 14 | { 15 | _onDispose = onDispose; 16 | } 17 | 18 | public void Dispose() => _onDispose?.Invoke(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Tests/Helpers/TestEntity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Chinook.DynamicMvvm.Tests.Helpers 8 | { 9 | public class TestEntity 10 | { 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Tests/Helpers/TestSubscriber.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Chinook.DynamicMvvm.Tests.Helpers 8 | { 9 | public class TestSubscriber : IObserver 10 | { 11 | private readonly Action _onNext; 12 | private readonly Action _onError; 13 | private readonly Action _onCompleted; 14 | 15 | public TestSubscriber(Action onNext = null, Action onError = null, Action onCompleted = null) 16 | { 17 | _onNext = onNext; 18 | _onError = onError; 19 | _onCompleted = onCompleted; 20 | } 21 | 22 | public void OnNext(T value) => _onNext?.Invoke(value); 23 | 24 | public void OnError(Exception error) => _onError?.Invoke(error); 25 | 26 | public void OnCompleted() => _onCompleted?.Invoke(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Tests/Helpers/TestViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Chinook.DynamicMvvm.Tests.Helpers 8 | { 9 | public class TestViewModel : ViewModelBase 10 | { 11 | private readonly Action _onDispose; 12 | 13 | public TestViewModel() 14 | { 15 | 16 | } 17 | 18 | public TestViewModel( 19 | string name = null, 20 | IServiceProvider serviceProvider = null, 21 | Action onDispose = null) 22 | : base(name, serviceProvider) 23 | { 24 | _onDispose = onDispose; 25 | } 26 | 27 | public TestEntity ReadWriteEntity 28 | { 29 | get => this.Get(); 30 | set => this.Set(value); 31 | } 32 | 33 | public TestEntity ReadEntity => this.Get(); 34 | 35 | protected override void Dispose(bool isDisposing) 36 | { 37 | base.Dispose(isDisposing); 38 | 39 | _onDispose?.Invoke(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Tests/Helpers/TestViewModelView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace Chinook.DynamicMvvm.Tests.Helpers 9 | { 10 | public class TestDispatcher : IDispatcher 11 | { 12 | private readonly bool _hasDispatcherAccess; 13 | private readonly Action _onExecuteOnDispatcher; 14 | 15 | public TestDispatcher( 16 | bool hasDispatcherAccess = false, 17 | Action onExecuteOnDispatcher = null 18 | ) 19 | { 20 | _hasDispatcherAccess = hasDispatcherAccess; 21 | _onExecuteOnDispatcher = onExecuteOnDispatcher; 22 | } 23 | 24 | public bool GetHasDispatcherAccess() => _hasDispatcherAccess; 25 | 26 | public Task ExecuteOnDispatcher(CancellationToken ct, Action action) 27 | { 28 | _onExecuteOnDispatcher?.Invoke(action); 29 | return Task.CompletedTask; 30 | } 31 | 32 | public void Dispose() 33 | { 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Tests/Integration/DeactivationIntegrationTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Linq; 5 | using System.Reactive.Linq; 6 | using System.Reactive.Subjects; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using Chinook.DynamicMvvm; 10 | using Chinook.DynamicMvvm.Deactivation; 11 | using FluentAssertions; 12 | using Xunit; 13 | 14 | namespace Chinook.DynamicMvvm.Tests.Integration 15 | { 16 | public class DeactivationIntegrationTests 17 | { 18 | [Fact] 19 | public void DeactivatableViewModelBase_works_with_DeactivatablePropertyFromObservable() 20 | { 21 | const string propertyName = nameof(TestVM.Count); 22 | var countChanged = false; 23 | var sut = new TestVM(); 24 | 25 | sut.PropertyChanged += OnPropertyChanged; 26 | sut.Count.Should().Be(0); 27 | 28 | // Update while activated. Updates should happen. 29 | sut.CountSubject.OnNext(1); 30 | ReadCountChanged().Should().BeTrue(); 31 | sut.CountSubject.HasObservers.Should().BeTrue(); 32 | sut.Count.Should().Be(1); 33 | 34 | // Deactivate. Updates should not happen and observable should not be subscribed to. 35 | sut.Deactivate(); 36 | sut.CountSubject.HasObservers.Should().BeFalse(); 37 | 38 | // Update while deactivated. 39 | sut.CountSubject.OnNext(2); 40 | sut.Count.Should().Be(1); 41 | ReadCountChanged().Should().BeFalse(); 42 | 43 | // Reactivate. The replay subject should be subscribed to and push an update. 44 | sut.Reactivate(); 45 | ReadCountChanged().Should().BeTrue(); 46 | sut.CountSubject.HasObservers.Should().BeTrue(); 47 | sut.Count.Should().Be(2); 48 | 49 | void OnPropertyChanged(object sender, PropertyChangedEventArgs e) 50 | { 51 | if (e.PropertyName == propertyName) 52 | { 53 | countChanged = true; 54 | } 55 | } 56 | 57 | bool ReadCountChanged() 58 | { 59 | var value = countChanged; 60 | countChanged = false; 61 | return value; 62 | } 63 | } 64 | 65 | [Fact] 66 | public void GetFromDeactivatableObservable_WithFuncOverload_doesnt_build_observable_more_than_once() 67 | { 68 | var sut = new TestVM2(); 69 | 70 | // InvocationCount should be 0 because the observable is not built until the first time it is accessed. 71 | sut.InvocationCount.Should().Be(0); 72 | sut.Count.Should().Be(0); 73 | 74 | // InvocationCount should be 1 because the observable is built the first time it is accessed. 75 | sut.InvocationCount.Should().Be(1); 76 | sut.Count.Should().Be(0); 77 | 78 | // InvocationCount should still be 1 because the property is cached. 79 | sut.InvocationCount.Should().Be(1); 80 | } 81 | 82 | public class TestVM : DeactivatableViewModelBase 83 | { 84 | public ReplaySubject CountSubject = new ReplaySubject(bufferSize: 1); 85 | 86 | public int Count => this.GetFromDeactivatableObservable(CountSubject, initialValue: 0); 87 | } 88 | 89 | public class TestVM2 : DeactivatableViewModelBase 90 | { 91 | public int InvocationCount { get; private set; } 92 | 93 | public int Count => this.GetFromDeactivatableObservable(GetObservable, initialValue: 0); 94 | 95 | private IObservable GetObservable() 96 | { 97 | ++InvocationCount; 98 | return Observable.Never(); 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Tests/Property/DynamicPropertyFactoryTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reactive.Subjects; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using FluentAssertions; 8 | using Chinook.DynamicMvvm.Tests.Helpers; 9 | using Xunit; 10 | 11 | namespace Chinook.DynamicMvvm.Tests.Property 12 | { 13 | public class DynamicPropertyFactoryTests 14 | { 15 | private const string DefaultPropertyName = nameof(DefaultPropertyName); 16 | 17 | [Fact] 18 | public void It_Creates_With_No_Value() 19 | { 20 | var factory = new DynamicPropertyFactory(); 21 | 22 | var property = factory.Create(DefaultPropertyName); 23 | 24 | property.Value.Should().BeNull(); 25 | } 26 | 27 | [Fact] 28 | public void It_Creates_With_Value() 29 | { 30 | var myValue = new TestEntity(); 31 | var factory = new DynamicPropertyFactory(); 32 | 33 | var property = factory.Create(DefaultPropertyName, myValue); 34 | 35 | property.Value.Should().Be(myValue); 36 | } 37 | 38 | [Fact] 39 | public void It_Creates_From_Observable_With_No_Value() 40 | { 41 | var source = new Subject(); 42 | var factory = new DynamicPropertyFactory(); 43 | 44 | var property = factory.CreateFromObservable(DefaultPropertyName, source); 45 | 46 | property.Value.Should().BeNull(); 47 | } 48 | 49 | [Fact] 50 | public void It_Creates_From_Observable_With_Value() 51 | { 52 | var source = new Subject(); 53 | var myValue = new TestEntity(); 54 | var factory = new DynamicPropertyFactory(); 55 | 56 | var property = factory.CreateFromObservable(DefaultPropertyName, source, myValue); 57 | 58 | property.Value.Should().Be(myValue); 59 | } 60 | 61 | [Fact] 62 | public void It_Creates_From_Task_With_No_Value() 63 | { 64 | var source = new TaskCompletionSource(); 65 | var factory = new DynamicPropertyFactory(); 66 | 67 | var property = factory.CreateFromTask(DefaultPropertyName, _ => source.Task); 68 | 69 | property.Value.Should().BeNull(); 70 | } 71 | 72 | [Fact] 73 | public void It_Creates_From_Task_With_Value() 74 | { 75 | var source = new TaskCompletionSource(); 76 | var myValue = new TestEntity(); 77 | var factory = new DynamicPropertyFactory(); 78 | 79 | var property = factory.CreateFromTask(DefaultPropertyName, _ => source.Task, myValue); 80 | 81 | property.Value.Should().Be(myValue); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Tests/Property/DynamicPropertyFromObservableTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reactive.Linq; 5 | using System.Reactive.Subjects; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using FluentAssertions; 9 | using Chinook.DynamicMvvm.Tests.Helpers; 10 | using Xunit; 11 | 12 | namespace Chinook.DynamicMvvm.Tests.Property 13 | { 14 | public class DynamicPropertyFromObservableTests 15 | { 16 | private const string DefaultPropertyName = nameof(DefaultPropertyName); 17 | 18 | [Fact] 19 | public void It_Creates_With_NoValue() 20 | { 21 | var source = new Subject(); 22 | 23 | var property = new DynamicPropertyFromObservable(DefaultPropertyName, source); 24 | 25 | property.Value.Should().BeNull(); 26 | } 27 | 28 | [Fact] 29 | public void It_Creates_With_Value() 30 | { 31 | var source = new Subject(); 32 | var value = new TestEntity(); 33 | 34 | var property = new DynamicPropertyFromObservable(DefaultPropertyName, source, value); 35 | 36 | property.Value.Should().Be(value); 37 | } 38 | 39 | [Fact] 40 | public void It_Changes_Value() 41 | { 42 | var source = new Subject(); 43 | var value = new TestEntity(); 44 | 45 | var property = new DynamicPropertyFromObservable(DefaultPropertyName, source) 46 | { 47 | Value = value 48 | }; 49 | 50 | property.Value.Should().Be(value); 51 | } 52 | 53 | [Fact] 54 | public void It_Changes_Value_From_Source() 55 | { 56 | var source = new Subject(); 57 | var value = new TestEntity(); 58 | 59 | var property = new DynamicPropertyFromObservable(DefaultPropertyName, source); 60 | 61 | source.OnNext(value); 62 | 63 | property.Value.Should().Be(value); 64 | } 65 | 66 | [Fact] 67 | public void It_Changes_Value_From_Source_Then_Set() 68 | { 69 | var source = new Subject(); 70 | var value = new TestEntity(); 71 | 72 | var property = new DynamicPropertyFromObservable(DefaultPropertyName, source); 73 | 74 | source.OnNext(new TestEntity()); 75 | 76 | property.Value = value; 77 | 78 | property.Value.Should().Be(value); 79 | } 80 | 81 | [Fact] 82 | public void It_Raises_ValueChanged() 83 | { 84 | var source = new Subject(); 85 | var receivedValues = new List(); 86 | var property = new DynamicPropertyFromObservable(DefaultPropertyName, source); 87 | 88 | property.ValueChanged += OnValueChanged; 89 | 90 | source.OnNext(new TestEntity()); 91 | 92 | receivedValues.Count().Should().Be(1); 93 | receivedValues[0].Should().Be(property); 94 | 95 | void OnValueChanged(IDynamicProperty p) 96 | { 97 | receivedValues.Add(p); 98 | } 99 | } 100 | 101 | [Fact] 102 | public void It_Doesnt_Raise_ValueChanged_For_SameValue() 103 | { 104 | var source = new Subject(); 105 | var value = new TestEntity(); 106 | 107 | var receivedValues = new List(); 108 | var property = new DynamicPropertyFromObservable(DefaultPropertyName, source, value); 109 | 110 | property.ValueChanged += OnValueChanged; 111 | 112 | source.OnNext(value); 113 | 114 | receivedValues.Should().BeEmpty(); 115 | 116 | void OnValueChanged(IDynamicProperty p) 117 | { 118 | receivedValues.Add(p); 119 | } 120 | } 121 | 122 | [Fact] 123 | public void It_Doesnt_Set_Value_After_Disposed() 124 | { 125 | var source = new Subject(); 126 | var value = new TestEntity(); 127 | 128 | var property = new DynamicPropertyFromObservable(DefaultPropertyName, source); 129 | 130 | property.Dispose(); 131 | 132 | source.OnNext(value); 133 | 134 | property.Value.Should().NotBe(value); 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Tests/Property/DynamicPropertyReactiveTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Chinook.DynamicMvvm.Tests.Property 8 | { 9 | public class DynamicPropertyReactiveTests 10 | { 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Tests/Property/ValueChangedOnBackgroundTaskDynamicPropertyTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Chinook.DynamicMvvm.Implementations; 8 | using FluentAssertions; 9 | using Xunit; 10 | 11 | namespace Chinook.DynamicMvvm.Tests.Property 12 | { 13 | public class ValueChangedOnBackgroundTaskDynamicPropertyTests 14 | { 15 | [Fact] 16 | public async Task ValueChanged_is_raised_on_a_different_task_when_dispatcher_access_is_true() 17 | { 18 | var vm = new ViewModelBase 19 | { 20 | Dispatcher = new TestDispatcher() 21 | { 22 | HasDispatcherAccess = true 23 | } 24 | }; 25 | 26 | var syncContext = new TaskCompletionSource(); 27 | var thread = new TaskCompletionSource(); 28 | var mainContext = SynchronizationContext.Current; 29 | var mainThread = Thread.CurrentThread; 30 | var sut = new ValueChangedOnBackgroundTaskDynamicProperty("sut", vm); 31 | 32 | sut.ValueChanged += OnValueChanged; 33 | sut.Value = 1; 34 | 35 | var valueChangedContext = await syncContext.Task; 36 | var valueChangedThread = await thread.Task; 37 | 38 | valueChangedContext.Should().NotBe(mainContext); 39 | valueChangedThread.Should().NotBe(mainThread); 40 | 41 | void OnValueChanged(IDynamicProperty property) 42 | { 43 | syncContext.SetResult(SynchronizationContext.Current); 44 | thread.SetResult(Thread.CurrentThread); 45 | } 46 | } 47 | 48 | [Fact] 49 | public async Task ValueChanged_is_raised_on_the_same_task_when_dispatcher_access_is_false() 50 | { 51 | var vm = new ViewModelBase 52 | { 53 | Dispatcher = new TestDispatcher() 54 | { 55 | HasDispatcherAccess = false 56 | } 57 | }; 58 | 59 | var syncContext = new TaskCompletionSource(); 60 | var thread = new TaskCompletionSource(); 61 | var mainContext = SynchronizationContext.Current; 62 | var mainThread = Thread.CurrentThread; 63 | var sut = new ValueChangedOnBackgroundTaskDynamicProperty("sut", vm); 64 | 65 | sut.ValueChanged += OnValueChanged; 66 | sut.Value = 1; 67 | 68 | var valueChangedContext = await syncContext.Task; 69 | var valueChangedThread = await thread.Task; 70 | 71 | valueChangedContext.Should().Be(mainContext); 72 | valueChangedThread.Should().Be(mainThread); 73 | 74 | void OnValueChanged(IDynamicProperty property) 75 | { 76 | syncContext.SetResult(SynchronizationContext.Current); 77 | thread.SetResult(Thread.CurrentThread); 78 | } 79 | } 80 | 81 | private class TestDispatcher : IDispatcher 82 | { 83 | public bool HasDispatcherAccess { get; set; } 84 | 85 | public Task ExecuteOnDispatcher(CancellationToken ct, Action action) 86 | { 87 | action(); 88 | return Task.CompletedTask; 89 | } 90 | 91 | public bool GetHasDispatcherAccess() 92 | { 93 | return HasDispatcherAccess; 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Tests/Reactive/DeactivatableObservableTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reactive.Subjects; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Chinook.DynamicMvvm.Deactivation; 8 | using FluentAssertions; 9 | using Xunit; 10 | 11 | namespace Chinook.DynamicMvvm.Tests.Reactive 12 | { 13 | public class DeactivatableObservableTests 14 | { 15 | [Fact] 16 | public void Updates_are_paused_when_deactivated() 17 | { 18 | var updateCount = 0; 19 | var subject = new ReplaySubject(bufferSize: 1); 20 | var sut = new DeactivatableObservable(subject); 21 | 22 | sut.Subscribe(OnNext); 23 | 24 | updateCount.Should().Be(0); 25 | subject.OnNext(0); 26 | updateCount.Should().Be(1); 27 | 28 | sut.Deactivate(); 29 | 30 | subject.OnNext(0); 31 | // The previous update should not go through because the observable is deactivated. 32 | updateCount.Should().Be(1); 33 | 34 | sut.Reactivate(); 35 | // When we reactivate to the ReplaySubject, we should get an update (because it replays). 36 | updateCount.Should().Be(2); 37 | 38 | void OnNext(int obj) 39 | { 40 | ++updateCount; 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Tests/Reactive/DynamicPropertyExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reactive.Linq; 5 | using System.Reactive.Threading.Tasks; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using FluentAssertions; 9 | using Xunit; 10 | 11 | namespace Chinook.DynamicMvvm.Tests.Reactive 12 | { 13 | public class DynamicPropertyExtensionsTests 14 | { 15 | [Fact] 16 | public async Task The_first_value_of_GetAndObserve_is_the_current_value_of_the_property() 17 | { 18 | var property = new DynamicProperty("TestProperty", value: 0); 19 | var observable = property.GetAndObserve(); 20 | 21 | var firstValue = await observable.FirstAsync(); 22 | firstValue.Should().Be(0); 23 | 24 | property.Value = 1; 25 | 26 | var secondValue = await observable.FirstAsync(); 27 | secondValue.Should().Be(1); 28 | } 29 | 30 | [Fact] 31 | public async Task Observe_doesnt_yield_until_property_changes() 32 | { 33 | var property = new DynamicProperty("TestProperty", value: 0); 34 | var observable = property.Observe(); 35 | 36 | var task = observable.FirstAsync().ToTask(); 37 | 38 | Assert.True(task.Status == TaskStatus.Running 39 | || task.Status == TaskStatus.WaitingForActivation); 40 | 41 | property.Value = 1; 42 | 43 | var value = await task; 44 | value.Should().Be(1); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Tests/ViewModel/DeactivatableViewModelBaseTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Chinook.DynamicMvvm.Deactivation; 7 | using FluentAssertions; 8 | using Xunit; 9 | 10 | namespace Chinook.DynamicMvvm.Tests.ViewModel 11 | { 12 | public class DeactivatableViewModelBaseTests 13 | { 14 | [Fact] 15 | public void PropertyChanged_events_are_not_raised_while_deactivated_but_raised_when_reactivated() 16 | { 17 | var propertyChanges = new List(); 18 | var sut = new DeactivatableViewModelBase(); 19 | sut.PropertyChanged += OnPropertyChanged; 20 | sut.Deactivate(); 21 | 22 | sut.RaisePropertyChanged("Allo"); 23 | sut.RaisePropertyChanged("Hi"); 24 | sut.RaisePropertyChanged("Bonjour"); 25 | 26 | propertyChanges.Should().OnlyContain(s => s == "IsDeactivated"); 27 | 28 | sut.Reactivate(); 29 | 30 | propertyChanges.Should().Contain(new []{ "Allo", "Hi", "Bonjour"}); 31 | 32 | void OnPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) 33 | { 34 | propertyChanges.Add(e.PropertyName); 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Tests/ViewModel/ViewModelBaseCommandsTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using FluentAssertions; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Chinook.DynamicMvvm.Tests.Helpers; 9 | using Xunit; 10 | 11 | namespace Chinook.DynamicMvvm.Tests.ViewModel 12 | { 13 | public class ViewModelBaseCommandsTest 14 | { 15 | private readonly IServiceProvider _serviceProvider; 16 | 17 | public ViewModelBaseCommandsTest() 18 | { 19 | var serviceCollection = new ServiceCollection(); 20 | 21 | serviceCollection.AddSingleton(); 22 | 23 | _serviceProvider = serviceCollection.BuildServiceProvider(); 24 | } 25 | 26 | [Fact] 27 | public async Task It_Gets_From_Action() 28 | { 29 | var isExecuted = false; 30 | var viewModel = new ViewModelBase(serviceProvider: _serviceProvider); 31 | 32 | var command = viewModel.GetCommand(() => isExecuted = true); 33 | await command.Execute(); 34 | 35 | isExecuted.Should().BeTrue(); 36 | } 37 | 38 | [Fact] 39 | public async Task It_Gets_From_Action_T() 40 | { 41 | var receivedParameter = default(TestEntity); 42 | var viewModel = new ViewModelBase(serviceProvider: _serviceProvider); 43 | 44 | var command = viewModel.GetCommand(p => receivedParameter = p); 45 | 46 | var parameter = new TestEntity(); 47 | await command.Execute(parameter); 48 | 49 | receivedParameter.Should().Be(parameter); 50 | } 51 | 52 | [Fact] 53 | public async Task It_Gets_From_Task() 54 | { 55 | var isExecuted = false; 56 | var viewModel = new ViewModelBase(serviceProvider: _serviceProvider); 57 | 58 | var command = viewModel.GetCommandFromTask(async ct => isExecuted = true); 59 | 60 | await command.Execute(); 61 | 62 | isExecuted.Should().BeTrue(); 63 | } 64 | 65 | [Fact] 66 | public async Task It_Gets_From_Task_T() 67 | { 68 | var receivedParameter = default(TestEntity); 69 | var viewModel = new ViewModelBase(serviceProvider: _serviceProvider); 70 | 71 | var command = viewModel.GetCommandFromTask(async (ct, p) => receivedParameter = p); 72 | 73 | var parameter = new TestEntity(); 74 | await command.Execute(parameter); 75 | 76 | receivedParameter.Should().Be(parameter); 77 | } 78 | 79 | [Fact] 80 | public void It_Adds_Disposable() 81 | { 82 | var viewModel = new ViewModelBase(serviceProvider: _serviceProvider); 83 | 84 | var command = viewModel.GetCommand(() => { }); 85 | 86 | var isAdded = viewModel.TryGetDisposable(nameof(It_Adds_Disposable), out var _); 87 | 88 | isAdded.Should().BeTrue(); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Tests/ViewModel/ViewModelBasePropertyChangedTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using FluentAssertions; 8 | using Chinook.DynamicMvvm.Tests.Helpers; 9 | using Xunit; 10 | 11 | namespace Chinook.DynamicMvvm.Tests.ViewModel 12 | { 13 | public class ViewModelBasePropertyChangedTests 14 | { 15 | private const string DefaultPropertyName = nameof(DefaultPropertyName); 16 | 17 | [Fact] 18 | public void It_Raises_PropertyChanged() 19 | { 20 | var receivedValues = new List<(object, PropertyChangedEventArgs)>(); 21 | var viewModel = new ViewModelBase(); 22 | 23 | viewModel.PropertyChanged += OnPropertyChanged; 24 | 25 | viewModel.RaisePropertyChanged(DefaultPropertyName); 26 | 27 | receivedValues.Count().Should().Be(1); 28 | receivedValues[0].Item1.Should().Be(viewModel); 29 | receivedValues[0].Item2.PropertyName.Should().Be(DefaultPropertyName); 30 | 31 | void OnPropertyChanged(object sender, PropertyChangedEventArgs e) 32 | { 33 | receivedValues.Add((sender, e)); 34 | } 35 | } 36 | 37 | [Fact] 38 | public void It_Raises_PropertyChanged_OnDispatcher_If_Not_OnDispatcher() 39 | { 40 | var executedOnDispatcher = false; 41 | var receivedValues = new List<(object, PropertyChangedEventArgs)>(); 42 | 43 | var viewModel = new ViewModelBase 44 | { 45 | Dispatcher = new TestDispatcher( 46 | hasDispatcherAccess: false, 47 | onExecuteOnDispatcher: a => 48 | { 49 | executedOnDispatcher = true; 50 | 51 | a(); 52 | } 53 | ) 54 | }; 55 | 56 | viewModel.PropertyChanged += OnPropertyChanged; 57 | 58 | viewModel.RaisePropertyChanged(DefaultPropertyName); 59 | 60 | executedOnDispatcher.Should().BeTrue(); 61 | 62 | receivedValues.Count().Should().Be(1); 63 | receivedValues[0].Item1.Should().Be(viewModel); 64 | receivedValues[0].Item2.PropertyName.Should().Be(DefaultPropertyName); 65 | 66 | void OnPropertyChanged(object sender, PropertyChangedEventArgs e) 67 | { 68 | receivedValues.Add((sender, e)); 69 | } 70 | } 71 | 72 | [Fact] 73 | public void It_Doesnt_Raise_PropertyChanged_OnDispatcher_If_OnDispatcher() 74 | { 75 | var executedOnDispatcher = false; 76 | var receivedValues = new List<(object, PropertyChangedEventArgs)>(); 77 | 78 | var viewModel = new ViewModelBase 79 | { 80 | Dispatcher = new TestDispatcher( 81 | hasDispatcherAccess: true, 82 | onExecuteOnDispatcher: a => 83 | { 84 | executedOnDispatcher = true; 85 | 86 | a(); 87 | } 88 | ) 89 | }; 90 | 91 | viewModel.PropertyChanged += OnPropertyChanged; 92 | 93 | viewModel.RaisePropertyChanged(DefaultPropertyName); 94 | 95 | executedOnDispatcher.Should().BeFalse(); 96 | 97 | receivedValues.Count().Should().Be(1); 98 | receivedValues[0].Item1.Should().Be(viewModel); 99 | receivedValues[0].Item2.PropertyName.Should().Be(DefaultPropertyName); 100 | 101 | void OnPropertyChanged(object sender, PropertyChangedEventArgs e) 102 | { 103 | receivedValues.Add((sender, e)); 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Tests/ViewModel/ViewModelBaseServicesTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using FluentAssertions; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Xunit; 9 | 10 | namespace Chinook.DynamicMvvm.Tests.ViewModel 11 | { 12 | public class ViewModelBaseServicesTests : IDisposable 13 | { 14 | [Fact] 15 | public void It_Resolves_Service_When_ServiceProvider_From_Constructor() 16 | { 17 | var service = new MyService(); 18 | 19 | var serviceCollection = new ServiceCollection(); 20 | serviceCollection.AddSingleton(s => service); 21 | 22 | var serviceProvider = serviceCollection.BuildServiceProvider(); 23 | 24 | var viewModel = new ViewModelBase(serviceProvider: serviceProvider); 25 | 26 | var receivedService = viewModel.GetService(); 27 | 28 | receivedService.Should().Be(service); 29 | } 30 | 31 | [Fact] 32 | public void It_Resolves_Service_When_ServiceProvider_From_Default() 33 | { 34 | var service = new MyService(); 35 | 36 | var serviceCollection = new ServiceCollection(); 37 | serviceCollection.AddSingleton(s => service); 38 | 39 | var serviceProvider = serviceCollection.BuildServiceProvider(); 40 | 41 | ViewModelBase.DefaultServiceProvider = serviceProvider; 42 | 43 | var viewModel = new ViewModelBase(); 44 | 45 | var receivedService = viewModel.GetService(); 46 | 47 | receivedService.Should().Be(service); 48 | } 49 | 50 | [Fact] 51 | public void It_Doesnt_Resolve_If_No_ServiceProvider() 52 | { 53 | var viewModel = new ViewModelBase(); 54 | 55 | Assert.ThrowsAny(() => viewModel.GetService()); 56 | } 57 | 58 | [Fact] 59 | public void It_Resolves_Using_Parameter() 60 | { 61 | var service = new MyService(); 62 | 63 | var serviceCollection = new ServiceCollection(); 64 | serviceCollection.AddSingleton(s => service); 65 | 66 | var serviceProvider = serviceCollection.BuildServiceProvider(); 67 | 68 | var viewModel = new ViewModelBase(serviceProvider: serviceProvider); 69 | 70 | var receivedService = viewModel.GetService(typeof(MyService)); 71 | 72 | receivedService.Should().Be(service); 73 | } 74 | 75 | public void Dispose() 76 | { 77 | ViewModelBase.DefaultServiceProvider = null; 78 | } 79 | 80 | private class MyService { } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Tests/ViewModel/ViewModelBaseTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Xunit; 7 | 8 | namespace Chinook.DynamicMvvm.Tests.ViewModel 9 | { 10 | public class ViewModelBaseTests 11 | { 12 | [Fact] 13 | public void It_Can_Be_Created_Without_Parameters() 14 | { 15 | new ViewModelBase(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Uno.WinUI/Dispatchers/DispatcherFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.UI.Xaml; 2 | 3 | namespace Chinook.DynamicMvvm 4 | { 5 | /// 6 | /// This is the default implementation of . 7 | /// It uses the implementation by default. 8 | /// 9 | [Preserve(AllMembers = true)] 10 | public class DispatcherFactory : IDispatcherFactory 11 | { 12 | private readonly CreateDispatcher _createDispatcher; 13 | 14 | /// 15 | /// Creates a new instance of . 16 | /// 17 | /// 18 | /// The optional method to use to generate the . 19 | /// When not provided, a method using the implementation is used. 20 | /// 21 | public DispatcherFactory(CreateDispatcher createDispatcher = null) 22 | { 23 | _createDispatcher = createDispatcher ?? CreateFromDispatcherQueue; 24 | } 25 | 26 | /// 27 | public IDispatcher Create(object view) 28 | { 29 | return _createDispatcher(view); 30 | } 31 | 32 | private IDispatcher CreateFromDispatcherQueue(object view) 33 | { 34 | return new DispatcherQueueDispatcher((FrameworkElement)view); 35 | } 36 | } 37 | 38 | /// 39 | /// This deletage is used to create an from a native view object. 40 | /// 41 | /// The native view object. 42 | /// A instance. 43 | public delegate IDispatcher CreateDispatcher(object view); 44 | } 45 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Uno.WinUI/Dispatchers/DispatcherQueueDispatcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Windows.UI.Core; 3 | using Microsoft.Extensions.Logging; 4 | using System.Threading.Tasks; 5 | using System.Threading; 6 | using Microsoft.UI.Dispatching; 7 | using Microsoft.UI.Xaml; 8 | 9 | namespace Chinook.DynamicMvvm 10 | { 11 | /// 12 | /// This implementation of uses . 13 | /// 14 | public class DispatcherQueueDispatcher : IDispatcher 15 | { 16 | private readonly DispatcherQueue _dispatcherQueue; 17 | 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | /// The from which to retrieve the . 22 | public DispatcherQueueDispatcher(FrameworkElement frameworkElement) 23 | { 24 | if (frameworkElement is null) 25 | { 26 | throw new ArgumentNullException(nameof(frameworkElement)); 27 | } 28 | 29 | _dispatcherQueue = frameworkElement.DispatcherQueue; 30 | } 31 | 32 | /// 33 | /// Initializes a new instance of the class. 34 | /// 35 | /// The . 36 | public DispatcherQueueDispatcher(DispatcherQueue dispatcherQueue) 37 | { 38 | if (dispatcherQueue is null) 39 | { 40 | throw new ArgumentNullException(nameof(dispatcherQueue)); 41 | } 42 | 43 | _dispatcherQueue = dispatcherQueue; 44 | } 45 | 46 | /// 47 | public bool GetHasDispatcherAccess() => _dispatcherQueue.HasThreadAccess; 48 | 49 | /// 50 | public async Task ExecuteOnDispatcher(CancellationToken ct, Action action) 51 | { 52 | if (ct.IsCancellationRequested) 53 | { 54 | this.Log().LogCancelledExecuteOnDispatcherBecauseOfCancellationToken(); 55 | return; 56 | } 57 | 58 | _dispatcherQueue.TryEnqueue(DispatcherQueuePriority.High, () => 59 | { 60 | try 61 | { 62 | if (ct.IsCancellationRequested) 63 | { 64 | this.Log().LogCancelledExecuteOnDispatcherBecauseOfCancellationToken(); 65 | return; 66 | } 67 | 68 | action(); 69 | } 70 | catch (Exception e) 71 | { 72 | this.Log().LogFailedExecuteOnDispatcher(e); 73 | } 74 | }); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Uno.WinUI/DynamicMvvm.Uno.WinUI.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net8.0;net8.0-windows10.0.19041;net8.0-android;net8.0-ios;net8.0-macos;net8.0-maccatalyst; 4 | 12.0 5 | 6 | true 7 | Chinook.DynamicMvvm 8 | nventive 9 | nventive 10 | Chinook.DynamicMvvm.Uno.WinUI 11 | Chinook.DynamicMvvm.Uno.WinUI 12 | Chinook.DynamicMvvm is a collection of extensible MVVM libraries for declarative ViewModels. 13 | true 14 | README.md 15 | mvvm;ios;android;chinook;maui;winui; 16 | Apache-2.0 17 | https://github.com/nventive/Chinook.DynamicMvvm 18 | $(DefineConstants);WINUI 19 | 20 | 21 | true 22 | true 23 | true 24 | snupkg 25 | 26 | 27 | 28 | 29 | True 30 | \ 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | $(DefineConstants);__WASM__ 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | win-x86;win-x64;win-arm64 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Uno.WinUI/Extensions/DispatcherQueueExtensions.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // https://github.com/CommunityToolkit/WindowsCommunityToolkit/blob/main/License.md 4 | // See reference: https://github.com/CommunityToolkit/WindowsCommunityToolkit/blob/main/Microsoft.Toolkit.Uwp/Extensions/DispatcherQueueExtensions.cs 5 | 6 | using System; 7 | using System.Threading.Tasks; 8 | using Microsoft.UI.Dispatching; 9 | using System.Runtime.CompilerServices; 10 | 11 | namespace Chinook.DynamicMvvm.Extensions 12 | { 13 | /// 14 | /// This class exposes extensions methods on . 15 | /// 16 | internal static class DispatcherQueueExtensions 17 | { 18 | /// 19 | /// Invokes a given function on the target and returns a 20 | /// that completes when the invocation of the function is completed. 21 | /// 22 | /// The target to invoke the code on. 23 | /// The to invoke. 24 | /// The priority level for the function to invoke. 25 | /// A that completes when the invocation of is over. 26 | /// If the current thread has access to , will be invoked directly. 27 | internal static Task RunAsync(this DispatcherQueue dispatcher, DispatcherQueuePriority priority = DispatcherQueuePriority.Normal, DispatcherQueueHandler handler = default) 28 | { 29 | // Run the function directly when we have thread access. 30 | // Also reuse Task.CompletedTask in case of success, 31 | // to skip an unnecessary heap allocation for every invocation. 32 | if (dispatcher.HasThreadAccess) 33 | { 34 | try 35 | { 36 | handler.Invoke(); 37 | 38 | return Task.CompletedTask; 39 | } 40 | catch (Exception e) 41 | { 42 | return Task.FromException(e); 43 | } 44 | } 45 | 46 | return TryEnqueueAsync(dispatcher, handler, priority); 47 | } 48 | 49 | internal static Task TryEnqueueAsync(DispatcherQueue dispatcher, DispatcherQueueHandler handler, DispatcherQueuePriority priority) 50 | { 51 | var taskCompletionSource = new TaskCompletionSource(); 52 | 53 | if (!dispatcher.TryEnqueue(priority, () => 54 | { 55 | try 56 | { 57 | handler(); 58 | 59 | taskCompletionSource.SetResult(null); 60 | } 61 | catch (Exception e) 62 | { 63 | taskCompletionSource.SetException(e); 64 | } 65 | })) 66 | { 67 | taskCompletionSource.SetException(GetEnqueueException("Failed to enqueue the operation")); 68 | } 69 | 70 | return taskCompletionSource.Task; 71 | } 72 | 73 | /// 74 | /// Creates an to return when an enqueue operation fails. 75 | /// 76 | /// The message of the exception. 77 | /// An with a specified message. 78 | [MethodImpl(MethodImplOptions.NoInlining)] 79 | internal static InvalidOperationException GetEnqueueException(string message) 80 | { 81 | return new InvalidOperationException(message); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Uno.WinUI/LoggerMessages.Uno.WinUI.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace Chinook.DynamicMvvm 9 | { 10 | internal static partial class LoggerMessagesUnoWinUI 11 | { 12 | [LoggerMessage(401, LogLevel.Debug, "Cancelled 'ExecuteOnDispatcher' because of the cancellation token.")] 13 | public static partial void LogCancelledExecuteOnDispatcherBecauseOfCancellationToken(this ILogger logger); 14 | 15 | [LoggerMessage(402, LogLevel.Debug, "Executed action immediately because already on dispatcher.")] 16 | public static partial void LogExecutedActionImmediatelyBecauseAlreadyOnDispatcher(this ILogger logger); 17 | 18 | [LoggerMessage(403, LogLevel.Debug, "Batched {RequestCount} dispatcher requests.")] 19 | public static partial void LogBatchedDispatcherRequests(this ILogger logger, int requestCount); 20 | 21 | [LoggerMessage(404, LogLevel.Error, "Failed 'ExecuteOnDispatcher'.")] 22 | public static partial void LogFailedExecuteOnDispatcher(this ILogger logger, Exception exception); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Uno.WinUI/ViewModel/IViewModel.Extensions.Uno.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.UI.Xaml; 2 | 3 | namespace Chinook.DynamicMvvm 4 | { 5 | /// 6 | /// Extensions on for Uno. 7 | /// 8 | public static class UnoViewModelExtensions 9 | { 10 | /// 11 | /// Attaches a to a by setting the using the implementation. 12 | /// 13 | /// 14 | /// 15 | public static void AttachToView(this IViewModel viewModel, FrameworkElement frameworkElement) 16 | { 17 | viewModel.Dispatcher = new DispatcherQueueDispatcher(frameworkElement); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/DynamicMvvm.Uno.WinUI/winappsdk-workaround.targets: -------------------------------------------------------------------------------- 1 |  2 | 9 | 10 | 11 | 12 | <_OtherPriFiles Include="@(PackagingOutputs)" Condition="'%(Extension)' == '.pri' and ('%(PackagingOutputs.ReferenceSourceTarget)' == 'ProjectReference' or '%(PackagingOutputs.NugetSourceType)'=='Package')" /> 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | <_OtherPriFiles1 Include="@(_ReferenceRelatedPaths)" Condition="'%(Extension)' == '.pri' and ('%(_ReferenceRelatedPaths.ReferenceSourceTarget)' == 'ProjectReference' or '%(_ReferenceRelatedPaths.NugetSourceType)'=='Package')" /> 21 | <_ReferenceRelatedPaths Remove="@(_OtherPriFiles1)" /> 22 | 23 | <_OtherPriFiles2 Include="@(ReferenceCopyLocalPaths)" Condition="'%(Extension)' == '.pri' and ('%(ReferenceCopyLocalPaths.ReferenceSourceTarget)' == 'ProjectReference' or '%(ReferenceCopyLocalPaths.NugetSourceType)'=='Package')" /> 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/DynamicMvvm/Command/DynamicCommandBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace Chinook.DynamicMvvm 7 | { 8 | /// 9 | /// The default implementation of . 10 | /// 11 | public class DynamicCommandBuilder : IDynamicCommandBuilder 12 | { 13 | private IDynamicCommand _command; 14 | 15 | /// 16 | /// Creates a new instance of . 17 | /// 18 | /// The name of the command. 19 | /// The base strategy to use for the command. 20 | /// The that will own the newly created . 21 | public DynamicCommandBuilder(string name, IDynamicCommandStrategy baseStrategy, IViewModel viewModel = null) 22 | { 23 | if (string.IsNullOrEmpty(name)) 24 | { 25 | throw new ArgumentException($"'{nameof(name)}' cannot be null or empty", nameof(name)); 26 | } 27 | 28 | Name = name; 29 | ViewModel = viewModel; 30 | BaseStrategy = baseStrategy ?? throw new ArgumentNullException(nameof(baseStrategy)); 31 | } 32 | 33 | /// 34 | public string Name { get; } 35 | 36 | /// 37 | public IViewModel ViewModel { get; } 38 | 39 | /// 40 | public IDynamicCommandStrategy BaseStrategy { get; } 41 | 42 | /// 43 | public IList Strategies { get; set; } = new List(); 44 | 45 | /// 46 | public IDynamicCommand Build() 47 | { 48 | if (_command != null) 49 | { 50 | throw new InvalidOperationException("This builder already built a command. A DynamicCommandBuilder can only build a single command because strategy instances cannot be shared."); 51 | } 52 | 53 | var strategy = GetStrategy(BaseStrategy, Strategies); 54 | _command = new DynamicCommand(Name, strategy); 55 | 56 | return _command; 57 | } 58 | 59 | private static IDynamicCommandStrategy GetStrategy(IDynamicCommandStrategy baseStrategy, IList delegatingStrategies) 60 | { 61 | var strategy = baseStrategy; 62 | 63 | // We use 'Reverse' so that the first items in the builder wrap the ones added later on. 64 | foreach (var delegatingStrategy in delegatingStrategies.Reverse()) 65 | { 66 | // Stitch up all the delegating strategies together. 67 | delegatingStrategy.InnerStrategy = strategy; 68 | strategy = delegatingStrategy; 69 | } 70 | 71 | return strategy; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/DynamicMvvm/Command/DynamicCommandBuilderFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace Chinook.DynamicMvvm 8 | { 9 | /// 10 | /// This is a default implementation of . 11 | /// 12 | [Preserve(AllMembers = true)] 13 | public class DynamicCommandBuilderFactory : IDynamicCommandBuilderFactory 14 | { 15 | private readonly Func _defaultConfigure; 16 | 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | public DynamicCommandBuilderFactory(Func defaultConfigure = null) 21 | { 22 | _defaultConfigure = defaultConfigure; 23 | } 24 | 25 | /// 26 | /// Will create new instance of . 27 | /// 28 | /// Command name 29 | /// 30 | /// The that will own the newly created command. 31 | /// 32 | protected IDynamicCommandBuilder CreateBuilder(string name, IDynamicCommandStrategy strategy, IViewModel viewModel) 33 | { 34 | IDynamicCommandBuilder builder = new DynamicCommandBuilder(name, strategy, viewModel); 35 | 36 | if (_defaultConfigure != null) 37 | { 38 | builder = _defaultConfigure(builder); 39 | } 40 | 41 | return builder; 42 | } 43 | 44 | /// 45 | public virtual IDynamicCommandBuilder CreateFromAction(string name, Action execute, IViewModel viewModel = null) 46 | => CreateBuilder(name, new ActionCommandStrategy(execute), viewModel); 47 | 48 | /// 49 | public virtual IDynamicCommandBuilder CreateFromAction(string name, Action execute, IViewModel viewModel = null) 50 | => CreateBuilder(name, new ActionCommandStrategy(execute), viewModel); 51 | 52 | /// 53 | public virtual IDynamicCommandBuilder CreateFromTask(string name, Func execute, IViewModel viewModel = null) 54 | => CreateBuilder(name, new TaskCommandStrategy(execute), viewModel); 55 | 56 | /// 57 | public virtual IDynamicCommandBuilder CreateFromTask(string name, Func execute, IViewModel viewModel = null) 58 | => CreateBuilder(name, new TaskCommandStrategy(execute), viewModel); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/DynamicMvvm/Command/Strategies/ActionCommandStrategy.T.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace Chinook.DynamicMvvm 8 | { 9 | /// 10 | /// This will execute an action with 11 | /// a parameter of type . 12 | /// 13 | public class ActionCommandStrategy : ActionCommandStrategy 14 | { 15 | /// 16 | /// Initializes a new instance of the class. 17 | /// 18 | /// Action to execute 19 | public ActionCommandStrategy(Action execute) 20 | : base(p => execute((TParameter)p)) 21 | { 22 | if (execute == null) 23 | { 24 | throw new ArgumentNullException(nameof(execute)); 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/DynamicMvvm/Command/Strategies/ActionCommandStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace Chinook.DynamicMvvm 8 | { 9 | /// 10 | /// This will execute an action. 11 | /// 12 | public class ActionCommandStrategy : IDynamicCommandStrategy 13 | { 14 | private readonly Action _execute; 15 | 16 | /// 17 | /// Initializes a new instance of the class. 18 | /// 19 | /// Action to execute 20 | public ActionCommandStrategy(Action execute) 21 | { 22 | _execute = execute ?? throw new ArgumentNullException(nameof(execute)); 23 | } 24 | 25 | /// 26 | /// Initializes a new instance of the class. 27 | /// 28 | /// Action to execute 29 | public ActionCommandStrategy(Action execute) 30 | : this(_ => execute()) 31 | { 32 | if (execute == null) 33 | { 34 | throw new ArgumentNullException(nameof(execute)); 35 | } 36 | } 37 | 38 | /// 39 | public event EventHandler CanExecuteChanged; 40 | 41 | /// 42 | public bool CanExecute(object parameter, IDynamicCommand command) => true; 43 | 44 | /// 45 | public Task Execute(CancellationToken ct, object parameter, IDynamicCommand command) 46 | { 47 | _execute(parameter); 48 | 49 | return Task.CompletedTask; 50 | } 51 | 52 | /// 53 | public void Dispose() 54 | { 55 | CanExecuteChanged = null; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/DynamicMvvm/Command/Strategies/BackgroundCommandStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace Chinook.DynamicMvvm 8 | { 9 | public static partial class DynamicCommandStrategyExtensions 10 | { 11 | /// 12 | /// Will execute the command on a background thread. 13 | /// 14 | /// The builder. 15 | /// 16 | public static IDynamicCommandBuilder OnBackgroundThread(this IDynamicCommandBuilder builder) 17 | => builder.WithStrategy(new BackgroundCommandStrategy()); 18 | } 19 | 20 | /// 21 | /// This will execute the command on a background thread. 22 | /// 23 | public class BackgroundCommandStrategy : DelegatingCommandStrategy 24 | { 25 | /// 26 | /// Initializes a new instance of the class. 27 | /// 28 | public BackgroundCommandStrategy() 29 | { 30 | } 31 | 32 | /// 33 | public override Task Execute(CancellationToken ct, object parameter, IDynamicCommand command) 34 | { 35 | return Task.Run(() => base.Execute(ct, parameter, command)); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/DynamicMvvm/Command/Strategies/CanExecuteCommandStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using System.Windows.Input; 6 | 7 | namespace Chinook.DynamicMvvm 8 | { 9 | public static partial class DynamicCommandStrategyExtensions 10 | { 11 | /// 12 | /// Will attach the to the specified . 13 | /// 14 | /// The builder. 15 | /// that affects the CanExecute 16 | /// 17 | public static IDynamicCommandBuilder WithCanExecute(this IDynamicCommandBuilder builder, IDynamicProperty canExecute) 18 | => builder.WithStrategy(new CanExecuteCommandStrategy(canExecute)); 19 | } 20 | 21 | /// 22 | /// This will attach 23 | /// its to the value of a . 24 | /// 25 | public class CanExecuteCommandStrategy : DelegatingCommandStrategy 26 | { 27 | private readonly IDynamicProperty _canExecute; 28 | 29 | /// 30 | /// Initializes a new instance of the class. 31 | /// 32 | /// Can execute property 33 | public CanExecuteCommandStrategy(IDynamicProperty canExecute) 34 | { 35 | _canExecute = canExecute; 36 | 37 | _canExecute.ValueChanged += OnCanExecuteChanged; 38 | } 39 | 40 | public override IDynamicCommandStrategy InnerStrategy 41 | { 42 | get => base.InnerStrategy; 43 | set 44 | { 45 | if (base.InnerStrategy != null) 46 | { 47 | base.InnerStrategy.CanExecuteChanged -= OnInnerCanExecuteChanged; 48 | } 49 | 50 | base.InnerStrategy = value; 51 | 52 | if (base.InnerStrategy != null) 53 | { 54 | base.InnerStrategy.CanExecuteChanged += OnInnerCanExecuteChanged; 55 | } 56 | } 57 | } 58 | 59 | /// 60 | public override event EventHandler CanExecuteChanged; 61 | 62 | /// 63 | public override bool CanExecute(object parameter, IDynamicCommand command) 64 | { 65 | return _canExecute.Value && InnerStrategy.CanExecute(parameter, command); 66 | } 67 | 68 | private void RaiseCanExecuteChanged() 69 | { 70 | CanExecuteChanged?.Invoke(this, EventArgs.Empty); 71 | } 72 | 73 | private void OnCanExecuteChanged(IDynamicProperty property) 74 | { 75 | RaiseCanExecuteChanged(); 76 | } 77 | 78 | private void OnInnerCanExecuteChanged(object sender, EventArgs e) 79 | { 80 | RaiseCanExecuteChanged(); 81 | } 82 | 83 | /// 84 | public override void Dispose() 85 | { 86 | _canExecute.ValueChanged -= OnCanExecuteChanged; 87 | InnerStrategy.CanExecuteChanged -= OnInnerCanExecuteChanged; 88 | 89 | base.Dispose(); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/DynamicMvvm/Command/Strategies/CancelPreviousCommandStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace Chinook.DynamicMvvm 8 | { 9 | public static partial class DynamicCommandStrategyExtensions 10 | { 11 | /// 12 | /// Will cancel the previous command execution when executing the command. 13 | /// 14 | /// The builder. 15 | /// 16 | public static IDynamicCommandBuilder CancelPrevious(this IDynamicCommandBuilder builder) 17 | => builder.WithStrategy(new CancelPreviousCommandStrategy()); 18 | } 19 | 20 | /// 21 | /// This will cancel the previous command execution when executing the command. 22 | /// 23 | public class CancelPreviousCommandStrategy : DelegatingCommandStrategy 24 | { 25 | private CancellationTokenSource _cancellationTokenSource; 26 | 27 | /// 28 | /// Initializes a new instance of the class. 29 | /// 30 | public CancelPreviousCommandStrategy() 31 | { 32 | } 33 | 34 | /// 35 | public override async Task Execute(CancellationToken ct, object parameter, IDynamicCommand command) 36 | { 37 | TryCancelExecution(); 38 | 39 | _cancellationTokenSource = new CancellationTokenSource(); 40 | 41 | using (ct.Register(TryCancelExecution)) 42 | { 43 | await base.Execute(_cancellationTokenSource.Token, parameter, command); 44 | } 45 | } 46 | 47 | /// 48 | /// Will cancel the current execution if any. 49 | /// 50 | private void TryCancelExecution() 51 | { 52 | if (_cancellationTokenSource != null && !_cancellationTokenSource.IsCancellationRequested) 53 | { 54 | _cancellationTokenSource.Cancel(); 55 | _cancellationTokenSource.Dispose(); 56 | } 57 | } 58 | 59 | /// 60 | public override void Dispose() 61 | { 62 | TryCancelExecution(); 63 | 64 | base.Dispose(); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/DynamicMvvm/Command/Strategies/DisableWhileExecutingCommandStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace Chinook.DynamicMvvm 8 | { 9 | public static partial class DynamicCommandStrategyExtensions 10 | { 11 | /// 12 | /// Will disable the command while it's executing. 13 | /// 14 | /// The builder. 15 | /// 16 | public static IDynamicCommandBuilder DisableWhileExecuting(this IDynamicCommandBuilder builder) 17 | => builder.WithStrategy(new DisableWhileExecutingCommandStrategy()); 18 | } 19 | 20 | /// 21 | /// This will disable the command while it's executing. 22 | /// 23 | public class DisableWhileExecutingCommandStrategy : DelegatingCommandStrategy 24 | { 25 | public int _isExecuting; 26 | 27 | /// 28 | /// Initializes a new instance of the class. 29 | /// 30 | public DisableWhileExecutingCommandStrategy() 31 | { 32 | } 33 | 34 | public override IDynamicCommandStrategy InnerStrategy 35 | { 36 | get => base.InnerStrategy; 37 | set 38 | { 39 | if (base.InnerStrategy != null) 40 | { 41 | base.InnerStrategy.CanExecuteChanged -= OnInnerCanExecuteChanged; 42 | } 43 | 44 | base.InnerStrategy = value; 45 | 46 | if (base.InnerStrategy != null) 47 | { 48 | base.InnerStrategy.CanExecuteChanged += OnInnerCanExecuteChanged; 49 | } 50 | } 51 | } 52 | 53 | /// 54 | public override event EventHandler CanExecuteChanged; 55 | 56 | /// 57 | public override bool CanExecute(object parameter, IDynamicCommand command) 58 | { 59 | var isExecuting = _isExecuting == 1; 60 | 61 | return !isExecuting && InnerStrategy.CanExecute(parameter, command); 62 | } 63 | 64 | /// 65 | public override async Task Execute(CancellationToken ct, object parameter, IDynamicCommand command) 66 | { 67 | if (Interlocked.CompareExchange(ref _isExecuting, 1, 0) == 0) 68 | { 69 | try 70 | { 71 | RaiseCanExecuteChanged(); 72 | 73 | await base.Execute(ct, parameter, command); 74 | } 75 | finally 76 | { 77 | _isExecuting = 0; 78 | 79 | RaiseCanExecuteChanged(); 80 | } 81 | } 82 | } 83 | 84 | private void RaiseCanExecuteChanged() 85 | { 86 | CanExecuteChanged?.Invoke(this, EventArgs.Empty); 87 | } 88 | 89 | private void OnInnerCanExecuteChanged(object sender, EventArgs e) 90 | { 91 | RaiseCanExecuteChanged(); 92 | } 93 | 94 | /// 95 | public override void Dispose() 96 | { 97 | InnerStrategy.CanExecuteChanged -= OnInnerCanExecuteChanged; 98 | 99 | base.Dispose(); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/DynamicMvvm/Command/Strategies/ErrorHandlerCommandStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace Chinook.DynamicMvvm 8 | { 9 | public static partial class DynamicCommandStrategyExtensions 10 | { 11 | /// 12 | /// Will catch any exception thrown by the execution of the command and delegate it to the specified error handler. 13 | /// 14 | /// The builder. 15 | /// Error handler 16 | /// 17 | public static IDynamicCommandBuilder CatchErrors(this IDynamicCommandBuilder builder, IDynamicCommandErrorHandler errorHandler) 18 | => builder.WithStrategy(new ErrorHandlerCommandStrategy(errorHandler)); 19 | 20 | /// 21 | /// Will catch any exception thrown by the execution of the command and delegate it to the specified error handler. 22 | /// 23 | /// The builder. 24 | /// Error handler 25 | /// 26 | public static IDynamicCommandBuilder CatchErrors(this IDynamicCommandBuilder builder, Func errorHandler) 27 | => builder.WithStrategy(new ErrorHandlerCommandStrategy(new DynamicCommandErrorHandler(errorHandler))); 28 | } 29 | 30 | /// 31 | /// This will catch any exception 32 | /// that may be thrown during its execution and delegate the exception to an error handler. 33 | /// 34 | public class ErrorHandlerCommandStrategy : DelegatingCommandStrategy 35 | { 36 | private readonly IDynamicCommandErrorHandler _errorHandler; 37 | 38 | /// 39 | /// Initializes a new instance of the class. 40 | /// 41 | /// Error handler 42 | public ErrorHandlerCommandStrategy(IDynamicCommandErrorHandler errorHandler) 43 | { 44 | _errorHandler = errorHandler; 45 | } 46 | 47 | /// 48 | public override async Task Execute(CancellationToken ct, object parameter, IDynamicCommand command) 49 | { 50 | try 51 | { 52 | await base.Execute(ct, parameter, command); 53 | } 54 | catch (Exception e) 55 | { 56 | await _errorHandler.HandleError(ct, command, e); 57 | } 58 | } 59 | } 60 | 61 | public interface IDynamicCommandErrorHandler 62 | { 63 | /// 64 | /// Handles an error from a . 65 | /// 66 | /// 67 | /// 68 | /// 69 | /// 70 | Task HandleError(CancellationToken ct, IDynamicCommand command, Exception exception); 71 | } 72 | 73 | public class DynamicCommandErrorHandler : IDynamicCommandErrorHandler 74 | { 75 | private readonly Func _handlerFunction; 76 | 77 | /// 78 | /// Initializes a new instance of the class. 79 | /// 80 | /// Error handler function 81 | public DynamicCommandErrorHandler(Func handlerFunction) 82 | { 83 | _handlerFunction = handlerFunction ?? throw new ArgumentNullException(nameof(handlerFunction)); 84 | } 85 | 86 | /// 87 | public Task HandleError(CancellationToken ct, IDynamicCommand command, Exception exception) 88 | => _handlerFunction.Invoke(ct, command, exception); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/DynamicMvvm/Command/Strategies/LockCommandStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace Chinook.DynamicMvvm 8 | { 9 | public static partial class DynamicCommandStrategyExtensions 10 | { 11 | /// 12 | /// Will lock the command execution. 13 | /// 14 | /// The builder. 15 | /// 16 | public static IDynamicCommandBuilder Locked(this IDynamicCommandBuilder builder) 17 | => builder.WithStrategy(new LockCommandStrategy()); 18 | } 19 | 20 | /// 21 | /// This will lock the command execution. 22 | /// 23 | public class LockCommandStrategy : DelegatingCommandStrategy 24 | { 25 | private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); 26 | 27 | /// 28 | /// Initializes a new instance of the class. 29 | /// 30 | public LockCommandStrategy() 31 | { 32 | } 33 | 34 | /// 35 | public override async Task Execute(CancellationToken ct, object parameter, IDynamicCommand command) 36 | { 37 | try 38 | { 39 | await _semaphore.WaitAsync(ct); 40 | 41 | await base.Execute(ct, parameter, command); 42 | } 43 | finally 44 | { 45 | _semaphore.Release(); 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/DynamicMvvm/Command/Strategies/LoggerCommandStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace Chinook.DynamicMvvm 9 | { 10 | public static partial class DynamicCommandStrategyExtensions 11 | { 12 | /// 13 | /// Will add logs to the command execution. 14 | /// 15 | /// The builder. 16 | /// Optional; the desired logger. If null is passed, a new one will be created using . 17 | /// 18 | public static IDynamicCommandBuilder WithLogs(this IDynamicCommandBuilder builder, ILogger logger = null) 19 | => builder.WithStrategy(new LoggerCommandStrategy(logger ?? typeof(IDynamicCommand).Log())); 20 | } 21 | 22 | /// 23 | /// This will log the execution of the command. 24 | /// 25 | public class LoggerCommandStrategy : DelegatingCommandStrategy 26 | { 27 | private readonly ILogger _logger; 28 | 29 | /// 30 | /// Initializes a new instance of the class. 31 | /// 32 | /// 33 | public LoggerCommandStrategy(ILogger logger) 34 | { 35 | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 36 | } 37 | 38 | /// 39 | public override async Task Execute(CancellationToken ct, object parameter, IDynamicCommand command) 40 | { 41 | try 42 | { 43 | _logger.LogCommandExecuting(command.Name); 44 | 45 | await base.Execute(ct, parameter, command); 46 | 47 | _logger.LogCommandExecuted(command.Name); 48 | } 49 | catch (Exception e) 50 | { 51 | _logger.LogCommandFailed(command.Name, e); 52 | 53 | throw; 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/DynamicMvvm/Command/Strategies/RaiseCanExecuteOnDispatcherCommandStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading; 5 | using Chinook.DynamicMvvm; 6 | 7 | namespace Chinook.DynamicMvvm 8 | { 9 | /// 10 | /// This ensures that the event is raised using . 11 | /// 12 | public class RaiseCanExecuteOnDispatcherCommandStrategy : DelegatingCommandStrategy 13 | { 14 | private readonly WeakReference _viewModel; 15 | private readonly CancellationTokenSource _cts = new CancellationTokenSource(); 16 | 17 | /// 18 | /// Creates a new instance of . 19 | /// 20 | /// The from which to access the . 21 | /// cannot be null. 22 | public RaiseCanExecuteOnDispatcherCommandStrategy(IViewModel viewModel) 23 | { 24 | if (viewModel is null) 25 | { 26 | throw new ArgumentNullException(nameof(viewModel)); 27 | } 28 | 29 | _viewModel = new WeakReference(viewModel); 30 | } 31 | 32 | /// 33 | public override IDynamicCommandStrategy InnerStrategy 34 | { 35 | get => base.InnerStrategy; 36 | set 37 | { 38 | if (base.InnerStrategy != null) 39 | { 40 | base.InnerStrategy.CanExecuteChanged -= OnInnerCanExecuteChanged; 41 | } 42 | 43 | base.InnerStrategy = value; 44 | 45 | if (base.InnerStrategy != null) 46 | { 47 | base.InnerStrategy.CanExecuteChanged += OnInnerCanExecuteChanged; 48 | } 49 | } 50 | } 51 | 52 | /// 53 | public override event EventHandler CanExecuteChanged; 54 | 55 | private void OnInnerCanExecuteChanged(object sender, EventArgs e) 56 | { 57 | var hasVM = _viewModel.TryGetTarget(out var viewModel); 58 | 59 | if (!hasVM || viewModel.IsDisposed) 60 | { 61 | return; 62 | } 63 | 64 | // The event should be raised immediately when the view already has dispatcher access OR when there is no view. 65 | var shouldRaiseImmediately= viewModel.Dispatcher?.GetHasDispatcherAccess() ?? true; 66 | 67 | if (shouldRaiseImmediately) 68 | { 69 | RaiseCanExecuteChanged(); 70 | } 71 | else 72 | { 73 | _ = viewModel.Dispatcher.ExecuteOnDispatcher(_cts.Token, RaiseCanExecuteChanged); 74 | } 75 | 76 | void RaiseCanExecuteChanged() 77 | { 78 | CanExecuteChanged?.Invoke(this, EventArgs.Empty); 79 | } 80 | } 81 | 82 | /// 83 | public override void Dispose() 84 | { 85 | base.Dispose(); 86 | 87 | _cts.Cancel(); 88 | _cts.Dispose(); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/DynamicMvvm/Command/Strategies/SkipWhileExecutingCommandStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Chinook.DynamicMvvm; 7 | 8 | namespace Chinook.DynamicMvvm 9 | { 10 | public static partial class DynamicCommandStrategyExtensions 11 | { 12 | /// 13 | /// Will skip executions if the command is already executing. 14 | /// 15 | /// The builder. 16 | /// 17 | public static IDynamicCommandBuilder SkipWhileExecuting(this IDynamicCommandBuilder builder) 18 | => builder.WithStrategy(new SkipWhileExecutingCommandStrategy()); 19 | } 20 | 21 | /// 22 | /// This will skip executions if the command is already executing. 23 | /// 24 | public class SkipWhileExecutingCommandStrategy : DelegatingCommandStrategy 25 | { 26 | public int _isExecuting; 27 | 28 | /// 29 | /// Initializes a new instance of the class. 30 | /// 31 | public SkipWhileExecutingCommandStrategy() 32 | { 33 | } 34 | 35 | /// 36 | public override async Task Execute(CancellationToken ct, object parameter, IDynamicCommand command) 37 | { 38 | if (Interlocked.CompareExchange(ref _isExecuting, 1, 0) == 0) 39 | { 40 | try 41 | { 42 | await base.Execute(ct, parameter, command); 43 | } 44 | finally 45 | { 46 | _isExecuting = 0; 47 | } 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/DynamicMvvm/Command/Strategies/TaskCommandStrategy.T.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace Chinook.DynamicMvvm 8 | { 9 | /// 10 | /// This will execute a task with 11 | /// a parameter of type . 12 | /// 13 | public class TaskCommandStrategy : TaskCommandStrategy 14 | { 15 | /// 16 | /// Initializes a new instance of the class. 17 | /// 18 | /// Action to execute 19 | public TaskCommandStrategy(Func execute) 20 | : base((ct, p) => execute(ct, (TParameter)p)) 21 | { 22 | if (execute == null) 23 | { 24 | throw new ArgumentNullException(nameof(execute)); 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/DynamicMvvm/Command/Strategies/TaskCommandStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace Chinook.DynamicMvvm 8 | { 9 | /// 10 | /// This will execute a task. 11 | /// 12 | public class TaskCommandStrategy : IDynamicCommandStrategy 13 | { 14 | private readonly Func _execute; 15 | 16 | /// 17 | /// Initializes a new instance of the class. 18 | /// 19 | /// Action to execute 20 | public TaskCommandStrategy(Func execute) 21 | { 22 | _execute = execute ?? throw new ArgumentNullException(nameof(execute)); 23 | } 24 | 25 | /// 26 | /// Initializes a new instance of the class. 27 | /// 28 | /// Action to execute 29 | public TaskCommandStrategy(Func execute) 30 | : this((ct, _) => execute(ct)) 31 | { 32 | if (execute == null) 33 | { 34 | throw new ArgumentNullException(nameof(execute)); 35 | } 36 | } 37 | 38 | /// 39 | public bool CanExecute(object parameter, IDynamicCommand command) => true; 40 | 41 | /// 42 | public Task Execute(CancellationToken ct, object parameter, IDynamicCommand command) 43 | => _execute(ct, parameter); 44 | 45 | /// 46 | public event EventHandler CanExecuteChanged; 47 | 48 | /// 49 | public void Dispose() 50 | { 51 | CanExecuteChanged = null; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/DynamicMvvm/Deactivation/DeactivatableDynamicPropertyFromObservable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Chinook.DynamicMvvm.Implementations; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace Chinook.DynamicMvvm.Deactivation 8 | { 9 | /// 10 | /// This is an implementation of a using an that 11 | /// ensures is raised on a background thread and its observable source can be deactivated. 12 | /// 13 | /// Type of value 14 | public class DeactivatableDynamicPropertyFromObservable : ValueChangedOnBackgroundTaskDynamicProperty, IDeactivatable 15 | { 16 | private readonly IObservable _source; 17 | private readonly DynamicPropertyFromObservable.DynamicPropertyObserver _propertyObserver; 18 | 19 | private IDisposable _subscription; 20 | private bool _isDisposed; 21 | 22 | /// 23 | /// Initializes a new instance of the class. 24 | /// 25 | /// The name of the this property. 26 | /// The observable source. 27 | /// The used to determine dispatcher access. 28 | /// The initial value of this property. 29 | public DeactivatableDynamicPropertyFromObservable(string name, IObservable source, IViewModel viewModel, T initialValue = default) 30 | : base(name, viewModel, initialValue) 31 | { 32 | if (source is null) 33 | { 34 | throw new ArgumentNullException(nameof(source)); 35 | } 36 | 37 | _source = source; 38 | _propertyObserver = new DynamicPropertyFromObservable.DynamicPropertyObserver(this); 39 | _subscription = source.Subscribe(_propertyObserver); 40 | } 41 | 42 | /// 43 | public bool IsDeactivated { get; private set; } = false; 44 | 45 | /// 46 | protected override void Dispose(bool isDisposing) 47 | { 48 | if (_isDisposed) 49 | { 50 | return; 51 | } 52 | 53 | if (isDisposing && _subscription != null) 54 | { 55 | _subscription.Dispose(); 56 | } 57 | 58 | _isDisposed = true; 59 | 60 | base.Dispose(isDisposing); 61 | } 62 | 63 | /// 64 | public void Deactivate() 65 | { 66 | if (IsDeactivated) 67 | { 68 | return; 69 | } 70 | 71 | _subscription.Dispose(); 72 | 73 | IsDeactivated = true; 74 | 75 | typeof(IDeactivatable).Log().LogDeactivatedObservableSource(Name); 76 | } 77 | 78 | /// 79 | public void Reactivate() 80 | { 81 | if (!IsDeactivated) 82 | { 83 | return; 84 | } 85 | 86 | IsDeactivated = false; 87 | 88 | _subscription = _source.Subscribe(_propertyObserver); 89 | 90 | typeof(IDeactivatable).Log().LogReactivatedObservableSource(Name); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/DynamicMvvm/Deactivation/IDeactivatableViewModel.Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.CompilerServices; 4 | using System.Text; 5 | using Chinook.DynamicMvvm.Deactivation; 6 | 7 | namespace Chinook.DynamicMvvm 8 | { 9 | /// 10 | /// This class exposes extensions methods on . 11 | /// 12 | public static class DeactivatableViewModelExtensions 13 | { 14 | /// 15 | /// Gets or creates a attached to this .
16 | /// The underlying implements so the observation can be deactivated. 17 | ///
18 | /// The property type. 19 | /// The owning the property. 20 | /// The observable of values that feeds the property. 21 | /// The property's initial value. 22 | /// The property's name. 23 | /// The property's value. 24 | public static T GetFromDeactivatableObservable(this IDeactivatableViewModel viewModel, IObservable source, T initialValue = default, [CallerMemberName] string name = null) 25 | { 26 | return viewModel.Get(viewModel.GetOrCreateDynamicProperty(name, n => new DeactivatableDynamicPropertyFromObservable(name, source, viewModel, initialValue))); 27 | } 28 | 29 | /// 30 | /// Gets or creates a attached to this .
31 | /// The underlying implements so the observation can be deactivated. 32 | /// This overload uses a to avoid evaluating the observable sequence more than once (which can avoid memory allocations). 33 | ///
34 | /// The property type. 35 | /// The owning the property. 36 | /// The provider of the observable of values that feeds the property. 37 | /// The property's initial value. 38 | /// The property's name. 39 | /// The property's value. 40 | public static T GetFromDeactivatableObservable(this IDeactivatableViewModel viewModel, Func> sourceProvider, T initialValue = default, [CallerMemberName] string name = null) 41 | { 42 | return viewModel.Get(viewModel.GetOrCreateDynamicProperty(name, n => new DeactivatableDynamicPropertyFromObservable(name, sourceProvider(), viewModel, initialValue))); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/DynamicMvvm/DynamicMvvm.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 12 6 | Chinook.DynamicMvvm 7 | nventive 8 | nventive 9 | Chinook.DynamicMvvm 10 | Chinook.DynamicMvvm 11 | Chinook.DynamicMvvm is a collection of extensible MVVM libraries for declarative ViewModels. 12 | true 13 | README.md 14 | mvvm;ios;android;chinook;maui;winui; 15 | Apache-2.0 16 | https://github.com/nventive/Chinook.DynamicMvvm 17 | 18 | 19 | true 20 | true 21 | true 22 | snupkg 23 | 24 | 25 | 26 | 27 | True 28 | \ 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/DynamicMvvm/PreserveAttribute.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable IDE1006 // Naming Styles 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace Chinook.DynamicMvvm 7 | { 8 | /// 9 | /// This attribute is used to avoid the mono linker to remove members. 10 | /// 11 | [AttributeUsage(AttributeTargets.All)] 12 | internal sealed class PreserveAttribute : Attribute 13 | { 14 | public bool AllMembers; 15 | } 16 | } 17 | #pragma warning restore IDE1006 // Naming Styles 18 | -------------------------------------------------------------------------------- /src/DynamicMvvm/Property/DynamicProperty.T.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Chinook.DynamicMvvm 4 | { 5 | /// 6 | /// This is a default implementation of . 7 | /// 8 | public class DynamicProperty : DynamicProperty, IDynamicProperty 9 | { 10 | /// 11 | /// Initializes a new instance of the class. 12 | /// 13 | /// 14 | /// When setting after being disposed, will be thrown. 15 | /// 16 | /// Name 17 | /// Initial value 18 | public DynamicProperty(string name, T value = default) 19 | : base(name, value) 20 | { 21 | } 22 | 23 | /// 24 | /// Initializes a new instance of the class. 25 | /// 26 | /// Name 27 | /// Initial value 28 | /// Whether a should be thrown when is changed after being disposed. 29 | [Obsolete("This constructor is obsolete. The throwOnDisposed parameter is no longer used.", error: false)] 30 | public DynamicProperty(string name, bool throwOnDisposed, T value = default) 31 | : base(name, value) 32 | { 33 | } 34 | 35 | /// 36 | public new T Value 37 | { 38 | get => (T)((DynamicProperty)this).Value; 39 | set => ((DynamicProperty)this).Value = value; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/DynamicMvvm/Property/DynamicProperty.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | 4 | namespace Chinook.DynamicMvvm 5 | { 6 | /// 7 | /// This is a default implementation of . 8 | /// 9 | public class DynamicProperty : IDynamicProperty 10 | { 11 | private static readonly DiagnosticSource _diagnostics = new DiagnosticListener("Chinook.DynamicMvvm.IDynamicProperty"); 12 | 13 | private object _value; 14 | private bool _isDisposed; 15 | 16 | /// 17 | /// Initializes a new instance of the class. 18 | /// 19 | /// 20 | /// When setting after being disposed, will be thrown. 21 | /// 22 | /// Name 23 | /// Initial value 24 | public DynamicProperty(string name, object value = default) 25 | { 26 | Name = name; 27 | _value = value; 28 | 29 | if (_diagnostics.IsEnabled("Created")) 30 | { 31 | _diagnostics.Write("Created", Name); 32 | } 33 | } 34 | 35 | /// 36 | /// Initializes a new instance of the class. 37 | /// 38 | /// Name 39 | /// Initial value 40 | /// Whether a should be thrown when is changed after being disposed. 41 | [Obsolete("This constructor is obsolete. The throwOnDisposed parameter is no longer used.", error: false)] 42 | public DynamicProperty(string name, bool throwOnDisposed, object value = default) 43 | : this(name, value) 44 | { 45 | } 46 | 47 | /// 48 | public string Name { get; } 49 | 50 | /// 51 | public object Value 52 | { 53 | get => _value; 54 | set 55 | { 56 | if (_isDisposed) 57 | { 58 | this.Log().LogDynamicPropertySkippedValueSetterBecauseDisposed(Name); 59 | return; 60 | } 61 | 62 | if (!Equals(value, _value)) 63 | { 64 | _value = value; 65 | 66 | ValueChanged?.Invoke(this); 67 | } 68 | } 69 | } 70 | 71 | /// 72 | public event DynamicPropertyChangedEventHandler ValueChanged; 73 | 74 | /// 75 | public override string ToString() 76 | { 77 | return Name + " " + Value; 78 | } 79 | 80 | /// 81 | protected virtual void Dispose(bool isDisposing) 82 | { 83 | if (_isDisposed) 84 | { 85 | return; 86 | } 87 | 88 | if (isDisposing) 89 | { 90 | ValueChanged = null; 91 | } 92 | 93 | _isDisposed = true; 94 | 95 | if (_diagnostics.IsEnabled("Disposed")) 96 | { 97 | _diagnostics.Write("Disposed", Name); 98 | } 99 | } 100 | 101 | /// 102 | public void Dispose() 103 | { 104 | Dispose(isDisposing: true); 105 | 106 | // If diagnostics are enabled, don't suppress the finalizer. 107 | // This allows the differentiation between disposed and destroyed instances. 108 | if (!_diagnostics.IsEnabled("Destroyed")) 109 | { 110 | GC.SuppressFinalize(this); 111 | } 112 | } 113 | 114 | /// 115 | ~DynamicProperty() 116 | { 117 | Dispose(isDisposing: false); 118 | 119 | if (_diagnostics.IsEnabled("Destroyed")) 120 | { 121 | _diagnostics.Write("Destroyed", Name); 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/DynamicMvvm/Property/DynamicPropertyFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace Chinook.DynamicMvvm 6 | { 7 | /// 8 | /// This is a default implementation of . 9 | /// 10 | /// 11 | /// This implementation doesn't actually require the viewModel parameter. 12 | /// 13 | [Preserve(AllMembers = true)] 14 | public class DynamicPropertyFactory : IDynamicPropertyFactory 15 | { 16 | /// 17 | /// Initializes a new instance of the class. 18 | /// 19 | /// 20 | /// When setting after being disposed, will be thrown. 21 | /// 22 | public DynamicPropertyFactory() 23 | { 24 | } 25 | 26 | /// 27 | /// Initializes a new instance of the class. 28 | /// 29 | /// Whether a should be thrown when is changed after being disposed. 30 | [Obsolete("This constructor is obsolete. The throwOnDisposed parameter is no longer used.", error: false)] 31 | public DynamicPropertyFactory(bool throwOnDisposed) 32 | { 33 | } 34 | 35 | /// 36 | public virtual IDynamicProperty Create(string name, T initialValue = default, IViewModel viewModel = null) 37 | => new DynamicProperty(name, initialValue); 38 | 39 | /// 40 | public virtual IDynamicProperty CreateFromTask(string name, Func> source, T initialValue = default, IViewModel viewModel = null) 41 | => new DynamicPropertyFromTask(name, source, initialValue); 42 | 43 | /// 44 | public virtual IDynamicProperty CreateFromObservable(string name, IObservable source, T initialValue = default, IViewModel viewModel = null) 45 | => new DynamicPropertyFromObservable(name, source, initialValue); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/DynamicMvvm/Property/DynamicPropertyFromObservable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Chinook.DynamicMvvm 7 | { 8 | /// 9 | /// This is an implementation of a using an . 10 | /// 11 | /// Type of value 12 | public class DynamicPropertyFromObservable : DynamicProperty 13 | { 14 | private readonly DynamicPropertyObserver _propertyObserver; 15 | private readonly IDisposable _subscription; 16 | private bool _isDisposed; 17 | 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | /// 22 | /// When setting after being disposed, will be thrown. 23 | /// 24 | /// Name 25 | /// Source 26 | /// Initial value 27 | public DynamicPropertyFromObservable(string name, IObservable source, T initialValue = default) 28 | : base(name, initialValue) 29 | { 30 | if (source is null) 31 | { 32 | throw new ArgumentNullException(nameof(source)); 33 | } 34 | 35 | _propertyObserver = new DynamicPropertyObserver(this); 36 | _subscription = source.Subscribe(_propertyObserver); 37 | } 38 | 39 | /// 40 | /// Initializes a new instance of the class. 41 | /// 42 | /// Name 43 | /// Source 44 | /// Initial value 45 | /// Whether a should be thrown when is changed after being disposed. 46 | [Obsolete("This constructor is obsolete. The throwOnDisposed parameter is no longer used.", error: false)] 47 | public DynamicPropertyFromObservable(string name, IObservable source, bool throwOnDisposed, T initialValue = default) 48 | : this(name, source, initialValue) 49 | { 50 | } 51 | 52 | /// 53 | protected override void Dispose(bool isDisposing) 54 | { 55 | if (_isDisposed) 56 | { 57 | return; 58 | } 59 | 60 | if (isDisposing && _subscription != null) 61 | { 62 | _subscription.Dispose(); 63 | } 64 | 65 | _isDisposed = true; 66 | 67 | base.Dispose(isDisposing); 68 | } 69 | 70 | public class DynamicPropertyObserver : IObserver 71 | { 72 | private readonly IDynamicProperty _owner; 73 | 74 | public DynamicPropertyObserver(IDynamicProperty owner) 75 | { 76 | _owner = owner; 77 | } 78 | 79 | public void OnCompleted() 80 | { 81 | } 82 | 83 | public void OnError(Exception e) 84 | { 85 | this.Log().LogDynamicPropertySourceObservableSubscriptionFailed(_owner.Name, e); 86 | } 87 | 88 | public void OnNext(T value) 89 | { 90 | _owner.Value = value; 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/DynamicMvvm/Property/DynamicPropertyFromTask.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Chinook.DynamicMvvm 7 | { 8 | /// 9 | /// This is an implementation of a using a . 10 | /// 11 | /// Type of value 12 | public class DynamicPropertyFromTask : DynamicProperty 13 | { 14 | private readonly CancellationTokenSource _cancellationTokenSource; 15 | private bool _isDisposed; 16 | 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | /// 21 | /// When setting after being disposed, will be thrown. 22 | /// 23 | /// Name 24 | /// Source 25 | /// Initial value 26 | public DynamicPropertyFromTask(string name, Func> source, T initialValue = default) 27 | : base(name, initialValue) 28 | { 29 | if (source is null) 30 | { 31 | throw new ArgumentNullException(nameof(source)); 32 | } 33 | 34 | _cancellationTokenSource = new CancellationTokenSource(); 35 | 36 | _ = SetValueFromSource(_cancellationTokenSource.Token, source); 37 | } 38 | 39 | /// 40 | /// Initializes a new instance of the class. 41 | /// 42 | /// Name 43 | /// Source 44 | /// Initial value 45 | /// Whether a should be thrown when is changed after being disposed. 46 | [Obsolete("This constructor is obsolete. The throwOnDisposed parameter is no longer used.", error: false)] 47 | public DynamicPropertyFromTask(string name, Func> source, bool throwOnDisposed, T initialValue = default) 48 | : this(name, source, initialValue) 49 | { 50 | } 51 | 52 | private async Task SetValueFromSource(CancellationToken ct, Func> source) 53 | { 54 | //await Task.Run(async () => 55 | //{ 56 | try 57 | { 58 | var value = await source(ct); 59 | 60 | Value = value; 61 | } 62 | catch (Exception e) 63 | { 64 | this.Log().LogDynamicPropertySourceTaskFailed(Name, e); 65 | } 66 | //}); 67 | } 68 | 69 | /// 70 | protected override void Dispose(bool isDisposing) 71 | { 72 | if (_isDisposed) 73 | { 74 | return; 75 | } 76 | 77 | if (isDisposing) 78 | { 79 | _cancellationTokenSource.Cancel(); 80 | } 81 | 82 | _isDisposed = true; 83 | 84 | base.Dispose(isDisposing); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/DynamicMvvm/Property/ValueChangedOnBackgroundTask/ValueChangedOnBackgroundTaskDynamicPropertyFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace Chinook.DynamicMvvm.Implementations 6 | { 7 | /// 8 | /// This implementation of uses the base class for all methods. 9 | /// 10 | [Preserve(AllMembers = true)] 11 | public class ValueChangedOnBackgroundTaskDynamicPropertyFactory : IDynamicPropertyFactory 12 | { 13 | /// 14 | /// Initializes a new instance of the class. 15 | /// 16 | /// 17 | /// When setting after being disposed, will be thrown. 18 | /// 19 | public ValueChangedOnBackgroundTaskDynamicPropertyFactory() 20 | { 21 | } 22 | 23 | /// 24 | /// Initializes a new instance of the class. 25 | /// 26 | /// Whether a should be thrown when is changed after being disposed. 27 | [Obsolete("This constructor is obsolete. The throwOnDisposed parameter is no longer used.", error: false)] 28 | public ValueChangedOnBackgroundTaskDynamicPropertyFactory(bool throwOnDisposed = true) 29 | { 30 | } 31 | 32 | /// 33 | public virtual IDynamicProperty Create(string name, T initialValue = default, IViewModel viewModel = null) 34 | { 35 | return new ValueChangedOnBackgroundTaskDynamicProperty(name, viewModel, initialValue); 36 | } 37 | 38 | /// 39 | public virtual IDynamicProperty CreateFromObservable(string name, IObservable source, T initialValue = default, IViewModel viewModel = null) 40 | { 41 | return new ValueChangedOnBackgroundTaskDynamicPropertyFromObservable(name, source, viewModel, initialValue); 42 | } 43 | 44 | /// 45 | public virtual IDynamicProperty CreateFromTask(string name, Func> source, T initialValue = default, IViewModel viewModel = null) 46 | { 47 | return new ValueChangedOnBackgroundTaskDynamicPropertyFromTask(name, source, viewModel, initialValue); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/DynamicMvvm/Property/ValueChangedOnBackgroundTask/ValueChangedOnBackgroundTaskDynamicPropertyFromObservable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Chinook.DynamicMvvm.Implementations 4 | { 5 | /// 6 | /// This is an implementation of a using an that ensures is raised on a background thread. 7 | /// 8 | /// Type of value 9 | public class ValueChangedOnBackgroundTaskDynamicPropertyFromObservable : ValueChangedOnBackgroundTaskDynamicProperty 10 | { 11 | private readonly DynamicPropertyFromObservable.DynamicPropertyObserver _propertyObserver; 12 | private readonly IDisposable _subscription; 13 | private bool _isDisposed; 14 | 15 | /// 16 | /// Initializes a new instance of the class. 17 | /// 18 | /// 19 | /// When setting after being disposed, will be thrown. 20 | /// 21 | /// The name of the this property. 22 | /// Source 23 | /// The used to determine dispatcher access. 24 | /// The initial value of this property. 25 | public ValueChangedOnBackgroundTaskDynamicPropertyFromObservable(string name, IObservable source, IViewModel viewModel, T initialValue = default) 26 | : base(name, viewModel, initialValue) 27 | { 28 | if (source is null) 29 | { 30 | throw new ArgumentNullException(nameof(source)); 31 | } 32 | 33 | _propertyObserver = new DynamicPropertyFromObservable.DynamicPropertyObserver(this); 34 | _subscription = source.Subscribe(_propertyObserver); 35 | } 36 | 37 | /// 38 | /// Initializes a new instance of the class. 39 | /// 40 | /// The name of the this property. 41 | /// Source 42 | /// The used to determine dispatcher access. 43 | /// The initial value of this property. 44 | /// Whether a should be thrown when is changed after being disposed. 45 | [Obsolete("This constructor is obsolete. The throwOnDisposed parameter is no longer used.", error: false)] 46 | public ValueChangedOnBackgroundTaskDynamicPropertyFromObservable(string name, IObservable source, IViewModel viewModel, bool throwOnDisposed, T initialValue = default) 47 | : this(name, source, viewModel, initialValue) 48 | { 49 | } 50 | 51 | /// 52 | protected override void Dispose(bool isDisposing) 53 | { 54 | if (_isDisposed) 55 | { 56 | return; 57 | } 58 | 59 | if (isDisposing && _subscription != null) 60 | { 61 | _subscription.Dispose(); 62 | } 63 | 64 | _isDisposed = true; 65 | 66 | base.Dispose(isDisposing); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/DynamicMvvm/Property/ValueChangedOnBackgroundTask/ValueChangedOnBackgroundTaskDynamicPropertyFromTask.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Chinook.DynamicMvvm.Implementations 7 | { 8 | /// 9 | /// This is an implementation of a using a that ensures is raised on a background thread. 10 | /// 11 | /// Type of value 12 | public class ValueChangedOnBackgroundTaskDynamicPropertyFromTask : ValueChangedOnBackgroundTaskDynamicProperty 13 | { 14 | private readonly CancellationTokenSource _cancellationTokenSource; 15 | private bool _isDisposed; 16 | 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | /// 21 | /// When setting after being disposed, will be thrown. 22 | /// 23 | /// The name of the this property. 24 | /// The task source for this property. 25 | /// The used to determine dispatcher access. 26 | /// The initial value of this property. 27 | public ValueChangedOnBackgroundTaskDynamicPropertyFromTask(string name, Func> source, IViewModel viewModel, T initialValue = default) 28 | : base(name, viewModel, initialValue) 29 | { 30 | if (source is null) 31 | { 32 | throw new ArgumentNullException(nameof(source)); 33 | } 34 | 35 | _cancellationTokenSource = new CancellationTokenSource(); 36 | 37 | _ = SetValueFromSource(_cancellationTokenSource.Token, source); 38 | } 39 | 40 | /// 41 | /// Initializes a new instance of the class. 42 | /// 43 | /// The name of the this property. 44 | /// The task source for this property. 45 | /// The used to determine dispatcher access. 46 | /// The initial value of this property. 47 | /// Whether a should be thrown when is changed after being disposed. 48 | [Obsolete("This constructor is obsolete. The throwOnDisposed parameter is no longer used.", error: false)] 49 | public ValueChangedOnBackgroundTaskDynamicPropertyFromTask(string name, Func> source, IViewModel viewModel, bool throwOnDisposed, T initialValue = default) 50 | : this(name, source, viewModel, initialValue) 51 | { 52 | } 53 | 54 | private async Task SetValueFromSource(CancellationToken ct, Func> source) 55 | { 56 | try 57 | { 58 | var value = await source(ct); 59 | 60 | Value = value; 61 | } 62 | catch (Exception e) 63 | { 64 | this.Log().LogDynamicPropertySourceTaskFailed(Name, e); 65 | } 66 | } 67 | 68 | /// 69 | protected override void Dispose(bool isDisposing) 70 | { 71 | if (_isDisposed) 72 | { 73 | return; 74 | } 75 | 76 | if (isDisposing) 77 | { 78 | _cancellationTokenSource.Cancel(); 79 | _cancellationTokenSource.Dispose(); 80 | } 81 | 82 | _isDisposed = true; 83 | 84 | base.Dispose(isDisposing); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/DynamicMvvm/ViewModel/ViewModelBase.Dispatcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Chinook.DynamicMvvm 7 | { 8 | public partial class ViewModelBase 9 | { 10 | private IDispatcher _dispatcher; 11 | 12 | /// 13 | public IDispatcher Dispatcher 14 | { 15 | get => _dispatcher; 16 | set => SetDispatcher(value); 17 | } 18 | 19 | /// 20 | public event Action DispatcherChanged; 21 | 22 | private void SetDispatcher(IDispatcher dispatcher) 23 | { 24 | if (dispatcher != null) 25 | { 26 | // When the VM is disposed, we don't want to throw when setting a null dispatcher. 27 | ThrowIfDisposed(); 28 | } 29 | 30 | _dispatcher = dispatcher; 31 | 32 | if (_isDisposing) 33 | { 34 | _logger.LogViewModelSkippedMethodBecauseDisposing(nameof(DispatcherChanged), Name); 35 | return; 36 | } 37 | 38 | DispatcherChanged?.Invoke(dispatcher); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/DynamicMvvm/ViewModel/ViewModelBase.Errors.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Concurrent; 4 | using System.Collections.Generic; 5 | using System.ComponentModel; 6 | using System.Linq; 7 | using System.Text; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace Chinook.DynamicMvvm 11 | { 12 | public partial class ViewModelBase 13 | { 14 | private Dictionary> _errors; 15 | 16 | /// 17 | public event EventHandler ErrorsChanged; 18 | 19 | /// 20 | public bool HasErrors => _errors is null ? false : _errors.Values.SelectMany(x => x).Any(); 21 | 22 | /// 23 | public IEnumerable GetErrors(string propertyName) 24 | { 25 | var errors = Enumerable.Empty(); 26 | 27 | if (string.IsNullOrEmpty(propertyName)) 28 | { 29 | // Entity level errors. 30 | errors = _errors?.Values.SelectMany(s => s); 31 | } 32 | else 33 | { 34 | // Property level errors. 35 | _errors?.TryGetValue(propertyName, out errors); 36 | } 37 | 38 | return errors ?? Enumerable.Empty(); 39 | } 40 | 41 | /// 42 | public void SetErrors(string propertyName, IEnumerable errors) 43 | { 44 | ThrowIfDisposed(); 45 | 46 | if (_isDisposing) 47 | { 48 | _logger.LogViewModelSkippedMethodBecauseDisposing_PropertyName(nameof(SetErrors), GetType().Name, propertyName, Name); 49 | return; 50 | } 51 | 52 | EnsureErrorsAreInitialized(); 53 | _errors[propertyName] = errors; 54 | 55 | ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName)); 56 | } 57 | 58 | /// 59 | public void SetErrors(IDictionary> errors) 60 | { 61 | ThrowIfDisposed(); 62 | 63 | if (_isDisposing) 64 | { 65 | _logger.LogViewModelSkippedMethodBecauseDisposing(nameof(SetErrors), Name); 66 | return; 67 | } 68 | 69 | _errors = new Dictionary>(errors); 70 | 71 | ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName: null)); 72 | } 73 | 74 | /// 75 | public void ClearErrors(string propertyName = null) 76 | { 77 | ThrowIfDisposed(); 78 | 79 | if (_isDisposing) 80 | { 81 | _logger.LogViewModelSkippedMethodBecauseDisposing_PropertyName(nameof(ClearErrors), GetType().Name, propertyName, Name); 82 | return; 83 | } 84 | 85 | if (_errors is null) 86 | { 87 | // No errors to clear. 88 | return; 89 | } 90 | 91 | if (string.IsNullOrEmpty(propertyName)) 92 | { 93 | _errors.Clear(); 94 | } 95 | else 96 | { 97 | _errors[propertyName] = Enumerable.Empty(); 98 | } 99 | 100 | ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName)); 101 | } 102 | 103 | private void EnsureErrorsAreInitialized() 104 | { 105 | if (_errors is null) 106 | { 107 | _errors = new Dictionary>(); 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/DynamicMvvm/ViewModel/ViewModelBase.PropertyChanged.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Threading; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Chinook.DynamicMvvm 7 | { 8 | public partial class ViewModelBase 9 | { 10 | /// 11 | public event PropertyChangedEventHandler PropertyChanged; 12 | 13 | /// 14 | public virtual void RaisePropertyChanged(string propertyName) 15 | { 16 | if (_isDisposing) 17 | { 18 | _logger.LogViewModelSkippedMethodBecauseDisposing_PropertyName(nameof(RaisePropertyChanged), GetType().Name, propertyName, Name); 19 | return; 20 | } 21 | 22 | if (_isDisposed) 23 | { 24 | _logger.LogViewModelSkippedMethodBecauseDisposed_PropertyName(nameof(RaisePropertyChanged), GetType().Name, propertyName, Name); 25 | return; 26 | } 27 | 28 | if (PropertyChanged == null) 29 | { 30 | return; 31 | } 32 | 33 | if (Dispatcher != null && !Dispatcher.GetHasDispatcherAccess()) 34 | { 35 | _ = Dispatcher.ExecuteOnDispatcher(CancellationToken, () => RaisePropertyChangedInner(propertyName)); 36 | } 37 | else 38 | { 39 | RaisePropertyChangedInner(propertyName); 40 | } 41 | } 42 | 43 | private void RaisePropertyChangedInner(string propertyName) 44 | { 45 | if (_isDisposing) 46 | { 47 | _logger.LogViewModelSkippedMethodBecauseDisposing_PropertyName(nameof(RaisePropertyChangedInner), GetType().Name, propertyName, Name); 48 | return; 49 | } 50 | 51 | if (_isDisposed) 52 | { 53 | _logger.LogViewModelSkippedMethodBecauseDisposed_PropertyName(nameof(RaisePropertyChangedInner), GetType().Name, propertyName, Name); 54 | return; 55 | } 56 | 57 | try 58 | { 59 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 60 | } 61 | catch (Exception exception) when (Dispatcher is null) 62 | { 63 | // Give some details and tips on how to fix the issue. 64 | _logger.LogViewModelFailedToRaisePropertyChangedWhenDispatcherIsNull(exception); 65 | throw; 66 | } 67 | 68 | _logger.LogViewModelRaisedPropertyChanged(propertyName, Name); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/DynamicMvvm/ViewModel/ViewModelBase.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Logging; 3 | using Microsoft.Extensions.Logging.Abstractions; 4 | using System; 5 | using System.Diagnostics; 6 | 7 | namespace Chinook.DynamicMvvm 8 | { 9 | /// 10 | /// This is a default implementation of . 11 | /// 12 | public partial class ViewModelBase : IViewModel 13 | { 14 | private static readonly DiagnosticSource _diagnostics = new DiagnosticListener("Chinook.DynamicMvvm.IViewModel"); 15 | 16 | private readonly ILogger _logger; 17 | 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | /// The name of the ViewModel. 22 | /// The service provider. 23 | public ViewModelBase(string name = null, IServiceProvider serviceProvider = null) 24 | { 25 | Name = name ?? GetType().Name; 26 | ServiceProvider = serviceProvider ?? DefaultServiceProvider; 27 | CancellationToken = _cts.Token; 28 | 29 | _logger = typeof(ViewModelBase).Log(); 30 | 31 | if (_diagnostics.IsEnabled("Created")) 32 | { 33 | _diagnostics.Write("Created", Name); 34 | } 35 | 36 | _logger.LogViewModelCreated(Name); 37 | } 38 | 39 | /// 40 | public string Name { get; } 41 | 42 | /// 43 | public IServiceProvider ServiceProvider { get; } 44 | 45 | /// 46 | /// Gets or sets the default . 47 | /// 48 | public static IServiceProvider DefaultServiceProvider { get; set; } 49 | } 50 | } 51 | --------------------------------------------------------------------------------